Skip to content

Instantly share code, notes, and snippets.

@marioosh
Created June 26, 2018 09:43
Show Gist options
  • Save marioosh/8350d0bbb8e59b06b07ca0cd20822635 to your computer and use it in GitHub Desktop.
Save marioosh/8350d0bbb8e59b06b07ca0cd20822635 to your computer and use it in GitHub Desktop.
HowToGraphql - Sangria tutorial - Authentication
package com.howtographql.scala.sangria
import com.howtographql.scala.sangria.models.Authorized
import sangria.execution.{Middleware, MiddlewareBeforeField, MiddlewareQueryContext}
import sangria.schema.Context
object AuthMiddleware extends Middleware[MyContext] with MiddlewareBeforeField[MyContext] {
override type QueryVal = Unit
override type FieldVal = Unit
override def beforeQuery(context: MiddlewareQueryContext[MyContext, _, _]) = ()
override def afterQuery(queryVal: QueryVal, context: MiddlewareQueryContext[MyContext, _, _]) = ()
override def beforeField(queryVal: QueryVal, mctx: MiddlewareQueryContext[MyContext, _, _], ctx: Context[MyContext, _]) = {
val requireAuth = ctx.field.tags contains Authorized
if(requireAuth) ctx.ctx.ensureAuthenticated()
continue
}
}
package com.howtographql.scala.sangria
import DBSchema._
import com.howtographql.scala.sangria.models.{AuthProviderSignupData, Link, User, Vote}
import sangria.execution.deferred.{RelationIds, SimpleRelation}
import slick.jdbc.H2Profile.api._
import scala.concurrent.Future
class DAO(db: Database) {
def allLinks = db.run(Links.result)
def getLinks(ids: Seq[Int]): Future[Seq[Link]] = db.run(
Links.filter(_.id inSet ids).result
)
def getLinksByUserIds(ids: Seq[Int]): Future[Seq[Link]] = {
db.run {
Links.filter(_.postedBy inSet ids).result
}
}
def getUsers(ids: Seq[Int]): Future[Seq[User]] = db.run(
Users.filter(_.id inSet ids).result
)
def getVotes(ids: Seq[Int]): Future[Seq[Vote]] = {
db.run(
Votes.filter(_.id inSet ids).result
)
}
def getVotesByRelationIds(rel: RelationIds[Vote]): Future[Seq[Vote]] =
db.run(
Votes.filter { vote =>
rel.rawIds.collect({
case (SimpleRelation("byUser"), ids: Seq[Int]) => vote.userId inSet ids
case (SimpleRelation("byLink"), ids: Seq[Int]) => vote.linkId inSet ids
}).foldLeft(true: Rep[Boolean])(_ || _)
} result
)
def createUser(name: String, authProvider: AuthProviderSignupData): Future[User] = {
val newUser = User(0, name, authProvider.email.email, authProvider.email.password )
val insertAndReturnUserQuery = (Users returning Users.map(_.id)) into {
(user, id) => user.copy(id = id)
}
db.run {
insertAndReturnUserQuery += newUser
}
}
def createLink(url: String, description: String, postedBy: Int): Future[Link] = {
val insertAndReturnLinkQuery = (Links returning Links.map(_.id)) into {
(link, id) => link.copy(id = id)
}
db.run {
insertAndReturnLinkQuery += Link(0, url, description, postedBy)
}
}
def createVote(linkId: Int, userId: Int): Future[Vote] = {
val insertAndReturnVoteQuery = (Votes returning Votes.map(_.id)) into {
(vote, id) => vote.copy(id = id)
}
db.run {
insertAndReturnVoteQuery += Vote(0, userId, linkId)
}
}
def authenticate(email: String, password: String): Future[Option[User]] = db.run {
Users.filter(u => u.email === email && u.password === password).result.headOption
}
}
package com.howtographql.scala.sangria
import akka.http.scaladsl.model.DateTime
import sangria.schema.{ListType, ObjectType}
import models._
import sangria.ast.StringValue
import sangria.execution.deferred.{DeferredResolver, Fetcher, Relation, RelationIds}
import sangria.schema._
import sangria.macros.derive._
object GraphQLSchema {
implicit val GraphQLDateTime = ScalarType[DateTime](//1
"DateTime",//2
coerceOutput = (dt, _) => dt.toString, //3
coerceInput = { //4
case StringValue(dt, _, _ ) => DateTime.fromIsoDateTimeString(dt).toRight(DateTimeCoerceViolation)
case _ => Left(DateTimeCoerceViolation)
},
coerceUserInput = { //5
case s: String => DateTime.fromIsoDateTimeString(s).toRight(DateTimeCoerceViolation)
case _ => Left(DateTimeCoerceViolation)
}
)
val IdentifiableType = InterfaceType(
"Identifiable",
fields[Unit, Identifiable](
Field("id", IntType, resolve = _.value.id)
)
)
lazy val LinkType: ObjectType[Unit, Link] = deriveObjectType[Unit, Link](
Interfaces(IdentifiableType),
ReplaceField("createdAt", Field("createdAt", GraphQLDateTime, resolve = _.value.createdAt)),
ReplaceField("postedBy",
Field("postedBy", UserType, resolve = c => usersFetcher.defer(c.value.postedBy))
),
AddFields(
Field("votes", ListType(VoteType), resolve = c => votesFetcher.deferRelSeq(voteByLinkRel, c.value.id))
)
)
lazy val UserType: ObjectType[Unit, User] = deriveObjectType[Unit, User](
Interfaces(IdentifiableType),
AddFields(
Field("links", ListType(LinkType),
resolve = c => linksFetcher.deferRelSeq(linkByUserRel, c.value.id)),
Field("votes", ListType(VoteType),
resolve = c => votesFetcher.deferRelSeq(voteByUserRel, c.value.id))
)
)
lazy val VoteType: ObjectType[Unit, Vote] = deriveObjectType[Unit, Vote](
Interfaces(IdentifiableType),
ExcludeFields("userId", "linkId"),
AddFields(Field("user", UserType, resolve = c => usersFetcher.defer(c.value.userId))),
AddFields(Field("link", LinkType, resolve = c => linksFetcher.defer(c.value.linkId)))
)
import sangria.marshalling.sprayJson._
import spray.json.DefaultJsonProtocol._
implicit val authProviderEmailFormat = jsonFormat2(AuthProviderEmail)
implicit val authProviderSignupDataFormat = jsonFormat1(AuthProviderSignupData)
implicit val AuthProviderEmailInputType: InputObjectType[AuthProviderEmail] = deriveInputObjectType[AuthProviderEmail](
InputObjectTypeName("AUTH_PROVIDER_EMAIL")
)
lazy val AuthProviderSignupDataInputType: InputObjectType[AuthProviderSignupData] = deriveInputObjectType[AuthProviderSignupData]()
val linkByUserRel = Relation[Link, Int]("byUser", l => Seq(l.postedBy))
val voteByLinkRel = Relation[Vote, Int]("byLink", v => Seq(v.linkId))
val voteByUserRel = Relation[Vote, Int]("byUser", v => Seq(v.userId))
val linksFetcher = Fetcher.rel(
(ctx: MyContext, ids: Seq[Int]) => ctx.dao.getLinks(ids),
(ctx: MyContext, ids: RelationIds[Link]) => ctx.dao.getLinksByUserIds(ids(linkByUserRel))
)
val usersFetcher = Fetcher(
(ctx: MyContext, ids: Seq[Int]) => ctx.dao.getUsers(ids)
)
val votesFetcher = Fetcher.rel(
(ctx: MyContext, ids: Seq[Int]) => ctx.dao.getVotes(ids),
(ctx: MyContext, ids: RelationIds[Vote]) => ctx.dao.getVotesByRelationIds(ids)
)
val Resolver = DeferredResolver.fetchers(linksFetcher, usersFetcher, votesFetcher)
val Id = Argument("id", IntType)
val Ids = Argument("ids", ListInputType(IntType))
val QueryType = ObjectType(
"Query",
fields[MyContext, Unit](
Field("allLinks", ListType(LinkType), resolve = c => c.ctx.dao.allLinks),
Field("link",
OptionType(LinkType),
arguments = Id :: Nil,
resolve = c => linksFetcher.deferOpt(c.arg(Id))
),
Field("links",
ListType(LinkType),
arguments = Ids :: Nil,
resolve = c => linksFetcher.deferSeq(c.arg(Ids))
),
Field("users",
ListType(UserType),
arguments = List(Ids),
resolve = c => usersFetcher.deferSeq(c.arg(Ids))
),
Field("votes",
ListType(VoteType),
arguments = List(Ids),
resolve = c => votesFetcher.deferSeq(c.arg(Ids))
)
)
)
val NameArg = Argument("name", StringType)
val AuthProviderArg = Argument("authProvider", AuthProviderSignupDataInputType)
val UrlArg = Argument("url", StringType)
val DescArg = Argument("description", StringType)
val PostedByArg = Argument("postedById", IntType)
val LinkIdArg = Argument("linkId", IntType)
val UserIdArg = Argument("userId", IntType)
val EmailArg = Argument("email", StringType)
val PasswordArg = Argument("password", StringType)
val Mutation = ObjectType(
"Mutation",
fields[MyContext, Unit](
Field("createUser",
UserType,
arguments = NameArg :: AuthProviderArg :: Nil,
resolve = c => c.ctx.dao.createUser(c.arg(NameArg), c.arg(AuthProviderArg))
),
Field("createLink",
LinkType,
arguments = UrlArg :: DescArg :: PostedByArg :: Nil,
tags = Authorized :: Nil,
resolve = c => c.ctx.dao.createLink(c.arg(UrlArg), c.arg(DescArg), c.arg(PostedByArg))),
Field("createVote",
VoteType,
arguments = LinkIdArg :: UserIdArg :: Nil,
resolve = c => c.ctx.dao.createVote(c.arg(LinkIdArg), c.arg(UserIdArg))),
Field("login",
UserType,
arguments = EmailArg :: PasswordArg :: Nil,
resolve = ctx => UpdateCtx(
ctx.ctx.login(ctx.arg(EmailArg), ctx.arg(PasswordArg))){ user =>
ctx.ctx.copy(currentUser = Some(user))
}
)
)
)
val SchemaDefinition = Schema(QueryType, Some(Mutation))
}
package com.howtographql.scala.sangria
import akka.http.scaladsl.server.{Route, _}
import sangria.parser.QueryParser
import spray.json.{JsObject, JsString, JsValue}
import akka.http.scaladsl.model.StatusCodes._
import akka.http.scaladsl.server.Directives._
import scala.concurrent.ExecutionContext
import scala.util.{Failure, Success}
import sangria.ast.Document
import sangria.execution.{ExceptionHandler => EHandler, _}
import akka.http.scaladsl.marshallers.sprayjson.SprayJsonSupport._
import com.howtographql.scala.sangria.models.{AuthenticationException, AuthorizationException}
import sangria.marshalling.sprayJson._
object GraphQLServer {
private val dao = DBSchema.createDatabase
def endpoint(requestJSON: JsValue)(implicit ec: ExecutionContext): Route = {
val JsObject(fields) = requestJSON
val JsString(query) = fields("query")
QueryParser.parse(query) match {
case Success(queryAst) =>
val operation = fields.get("operationName") collect {
case JsString(op) => op
}
val variables = fields.get("variables") match {
case Some(obj: JsObject) => obj
case _ => JsObject.empty
}
complete(executeGraphQLQuery(queryAst, operation, variables))
case Failure(error) =>
complete(BadRequest, JsObject("error" -> JsString(error.getMessage)))
}
}
val ErrorHandler = EHandler {
case (_, AuthenticationException(message)) ⇒ HandledException(message)
case (_, AuthorizationException(message)) ⇒ HandledException(message)
}
private def executeGraphQLQuery(query: Document, operation: Option[String], vars: JsObject)(implicit ec: ExecutionContext) = {
Executor.execute(
GraphQLSchema.SchemaDefinition,
query,
MyContext(dao),
variables = vars,
operationName = operation,
deferredResolver = GraphQLSchema.Resolver,
exceptionHandler = ErrorHandler,
middleware = AuthMiddleware :: Nil
).map(OK -> _)
.recover {
case error: QueryAnalysisError => BadRequest -> error.resolveError
case error: ErrorWithResolver => InternalServerError -> error.resolveError
}
}
}
package com.howtographql.scala.sangria
import com.howtographql.scala.sangria.models.{AuthenticationException, AuthorizationException, User}
import scala.concurrent._
import scala.concurrent.duration.Duration
case class MyContext(dao: DAO, currentUser: Option[User] = None){
def login(email: String, password: String): User = {
val userOpt = Await.result(dao.authenticate(email, password), Duration.Inf)
userOpt.getOrElse(
throw AuthenticationException("email or password are incorrect!")
)
}
def ensureAuthenticated() =
if(currentUser.isEmpty)
throw AuthorizationException("You do not have permission. Please sign in.")
}
package com.howtographql.scala.sangria
import akka.http.scaladsl.model.DateTime
import sangria.execution.FieldTag
import sangria.execution.deferred.HasId
import sangria.validation.Violation
package object models {
trait Identifiable {
val id: Int
}
object Identifiable {
implicit def hasId[T <: Identifiable]: HasId[T, Int] = HasId(_.id)
}
case class Link(id: Int, url: String, description: String, postedBy: Int, createdAt: DateTime = DateTime.now) extends Identifiable
case object DateTimeCoerceViolation extends Violation {
override def errorMessage: String = "Error during parsing DateTime"
}
case class User(id: Int, name: String, email: String, password: String, createdAt: DateTime = DateTime.now) extends Identifiable
case class Vote(id: Int, userId: Int, linkId: Int, createdAt: DateTime = DateTime.now) extends Identifiable
case class AuthProviderEmail(email: String, password: String)
case class AuthProviderSignupData(email: AuthProviderEmail)
case class AuthenticationException(message: String) extends Exception(message)
case class AuthorizationException(message: String) extends Exception(message)
case object Authorized extends FieldTag
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment