Created
June 26, 2018 09:43
-
-
Save marioosh/8350d0bbb8e59b06b07ca0cd20822635 to your computer and use it in GitHub Desktop.
HowToGraphql - Sangria tutorial - Authentication
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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)) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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