-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* WIP: add new project example + add QueryT combinator * Replace h2 by postgres * Example app: refactor + postman collection - Add more checks for POST actions - Better business case error handling - Add route to list todos - Add postman collection for api usage * WIP: Add auth + add dependent actions * Update postman collection + Add password hash + fix some actions * Update postman collection * Remove adminer + refactor authentication trait
- Loading branch information
Showing
22 changed files
with
1,011 additions
and
1 deletion.
There are no files selected for viewing
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
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
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
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,44 @@ | ||
package com.zengularity.querymonad.examples.todoapp.controller | ||
|
||
import java.util.UUID | ||
|
||
import scala.concurrent.{ExecutionContext, Future} | ||
import scala.util.Try | ||
|
||
import play.api.mvc._ | ||
|
||
trait Authentication { self: BaseController => | ||
|
||
implicit def ec: ExecutionContext | ||
|
||
case class ConnectedUserInfo( | ||
id: UUID, | ||
login: String | ||
) | ||
|
||
case class ConnectedUserRequest[A]( | ||
userInfo: ConnectedUserInfo, | ||
request: Request[A] | ||
) extends WrappedRequest[A](request) | ||
|
||
def ConnectedAction = Action andThen ConnectionRefiner | ||
|
||
private def ConnectionRefiner = | ||
new ActionRefiner[Request, ConnectedUserRequest] { | ||
def executionContext = ec | ||
def refine[A](request: Request[A]) = | ||
Future.successful( | ||
request.session | ||
.get("id") | ||
.flatMap(str => Try(UUID.fromString(str)).toOption) | ||
.zip(request.session.get("login")) | ||
.headOption | ||
.map { | ||
case (id, login) => | ||
ConnectedUserRequest(ConnectedUserInfo(id, login), request) | ||
} | ||
.toRight(Unauthorized("Missing credentials")) | ||
) | ||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,121 @@ | ||
package com.zengularity.querymonad.examples.todoapp.controller | ||
|
||
import java.util.UUID | ||
|
||
import scala.concurrent.{ExecutionContext, Future} | ||
|
||
import cats.instances.either._ | ||
import play.api.mvc._ | ||
import play.api.libs.json.Json | ||
|
||
import com.zengularity.querymonad.examples.todoapp.controller.model.AddTodoPayload | ||
import com.zengularity.querymonad.examples.todoapp.model.{Todo, User} | ||
import com.zengularity.querymonad.examples.todoapp.store.{TodoStore, UserStore} | ||
import com.zengularity.querymonad.module.sql.{SqlQueryRunner, SqlQueryT} | ||
|
||
class TodoController( | ||
runner: SqlQueryRunner, | ||
todoStore: TodoStore, | ||
userStore: UserStore, | ||
cc: ControllerComponents | ||
)(implicit val ec: ExecutionContext) | ||
extends AbstractController(cc) | ||
with Authentication { | ||
|
||
type ErrorOrResult[A] = Either[String, A] | ||
|
||
private def check( | ||
login: String | ||
)(block: => Future[Result])(implicit request: ConnectedUserRequest[_]) = { | ||
if (request.userInfo.login == login) | ||
block | ||
else | ||
Future.successful(BadRequest("Not authorized action")) | ||
} | ||
|
||
def addTodo(login: String): Action[AddTodoPayload] = | ||
ConnectedAction.async(parse.json[AddTodoPayload]) { implicit request => | ||
check(login) { | ||
val payload = request.body | ||
val todo = AddTodoPayload.toModel(payload)(UUID.randomUUID(), | ||
request.userInfo.id) | ||
val query = for { | ||
_ <- SqlQueryT.fromQuery[ErrorOrResult, Unit]( | ||
todoStore.getByNumber(todo.todoNumber).map { | ||
case Some(_) => Left("Todo already exists") | ||
case None => Right(()) | ||
} | ||
) | ||
_ <- SqlQueryT.liftQuery[ErrorOrResult, Unit]( | ||
todoStore.addTodo(todo) | ||
) | ||
} yield () | ||
|
||
runner(query).map { | ||
case Right(_) => NoContent | ||
case Left(description) => BadRequest(description) | ||
} | ||
} | ||
} | ||
|
||
def getTodo(login: String, todoId: UUID): Action[AnyContent] = | ||
ConnectedAction.async { implicit request => | ||
check(login) { | ||
runner(todoStore.getTodo(todoId)).map { | ||
case Some(todo) => Ok(Json.toJson(todo)) | ||
case None => NotFound | ||
} | ||
} | ||
} | ||
|
||
def listTodo(login: String): Action[AnyContent] = | ||
ConnectedAction.async { implicit request => | ||
check(login) { | ||
val query = for { | ||
user <- SqlQueryT.fromQuery[ErrorOrResult, User]( | ||
userStore.getByLogin(login).map(_.toRight("User doesn't exist")) | ||
) | ||
todo <- SqlQueryT.liftQuery[ErrorOrResult, List[Todo]]( | ||
todoStore.listTodo(user.id) | ||
) | ||
} yield todo | ||
|
||
runner(query).map { | ||
case Right(todos) => Ok(Json.toJson(todos)) | ||
case Left(description) => BadRequest(description) | ||
} | ||
} | ||
} | ||
|
||
def removeTodo(login: String, todoId: UUID): Action[AnyContent] = | ||
ConnectedAction.async { implicit request => | ||
check(login) { | ||
val query = for { | ||
- <- SqlQueryT.fromQuery[ErrorOrResult, Todo]( | ||
todoStore | ||
.getTodo(todoId) | ||
.map(_.toRight("Todo doesn't exist")) | ||
) | ||
_ <- SqlQueryT.liftQuery[ErrorOrResult, Unit]( | ||
todoStore.removeTodo(todoId) | ||
) | ||
} yield () | ||
|
||
runner(query).map { | ||
case Right(_) => NoContent | ||
case Left(description) => BadRequest(description) | ||
} | ||
} | ||
} | ||
|
||
def completeTodo(login: String, todoId: UUID): Action[AnyContent] = | ||
ConnectedAction.async { implicit request => | ||
check(login) { | ||
runner(todoStore.completeTodo(todoId)).map { | ||
case Some(todo) => Ok(Json.toJson(todo)) | ||
case None => NotFound("The todo doesn't exist") | ||
} | ||
} | ||
} | ||
|
||
} |
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
package com.zengularity.querymonad.examples.todoapp.controller | ||
|
||
import java.nio.charset.Charset | ||
import java.util.{Base64, UUID} | ||
|
||
import scala.concurrent.{ExecutionContext, Future} | ||
|
||
import cats.instances.either._ | ||
import play.api.mvc._ | ||
import play.api.libs.json.Json | ||
|
||
import com.zengularity.querymonad.examples.todoapp.controller.model.AddUserPayload | ||
import com.zengularity.querymonad.examples.todoapp.model.{Credential, User} | ||
import com.zengularity.querymonad.examples.todoapp.store.{ | ||
CredentialStore, | ||
UserStore | ||
} | ||
import com.zengularity.querymonad.module.sql.{SqlQueryRunner, SqlQueryT} | ||
|
||
class UserController( | ||
runner: SqlQueryRunner, | ||
store: UserStore, | ||
credentialStore: CredentialStore, | ||
cc: ControllerComponents | ||
)(implicit val ec: ExecutionContext) | ||
extends AbstractController(cc) | ||
with Authentication { | ||
|
||
type ErrorOrResult[A] = Either[String, A] | ||
|
||
def createUser: Action[AddUserPayload] = | ||
Action(parse.json[AddUserPayload]).async { implicit request => | ||
val payload = request.body | ||
val query = for { | ||
_ <- SqlQueryT.fromQuery[ErrorOrResult, Unit]( | ||
store.getByLogin(payload.login).map { | ||
case Some(_) => Left("User already exists") | ||
case None => Right(()) | ||
} | ||
) | ||
|
||
user = AddUserPayload.toModel(payload)(UUID.randomUUID()) | ||
credential = AddUserPayload.toCredential(payload) | ||
|
||
_ <- SqlQueryT.liftQuery[ErrorOrResult, Unit]( | ||
credentialStore.saveCredential(credential) | ||
) | ||
_ <- SqlQueryT.liftQuery[ErrorOrResult, Unit](store.createUser(user)) | ||
} yield () | ||
|
||
runner(query).map { | ||
case Right(_) => NoContent | ||
case Left(description) => BadRequest(description) | ||
} | ||
} | ||
|
||
def getUser(userId: UUID): Action[AnyContent] = ConnectedAction.async { | ||
request => | ||
if (request.userInfo.id == userId) | ||
runner(store.getUser(userId)).map { | ||
case Some(user) => Ok(Json.toJson(user)) | ||
case None => NotFound("The user doesn't exist") | ||
} else | ||
Future.successful(NotFound("Cannot operate this action")) | ||
} | ||
|
||
def deleteUser(userId: UUID): Action[AnyContent] = ConnectedAction.async { | ||
request => | ||
val userInfo = request.userInfo | ||
if (userInfo.id == userId) { | ||
val query = for { | ||
_ <- credentialStore.deleteCredentials(userInfo.login) | ||
_ <- store.deleteUser(userId) | ||
} yield () | ||
runner(query).map(_ => NoContent.withNewSession) | ||
} else | ||
Future.successful(BadRequest("Cannot operate this action")) | ||
} | ||
|
||
def login: Action[AnyContent] = Action.async { implicit request => | ||
val authHeaderOpt = request.headers | ||
.get("Authorization") | ||
.map(_.substring("Basic".length()).trim()) | ||
|
||
val query = for { | ||
credential <- SqlQueryT.liftF[ErrorOrResult, Credential]( | ||
authHeaderOpt | ||
.map { encoded => | ||
val decoded = Base64.getDecoder().decode(encoded) | ||
val authStr = new String(decoded, Charset.forName("UTF-8")) | ||
authStr.split(':').toList | ||
} | ||
.collect { | ||
case login :: password :: _ => Credential(login, password) | ||
} | ||
.toRight("Missing credentials") | ||
) | ||
|
||
exists <- SqlQueryT.liftQuery[ErrorOrResult, Boolean]( | ||
credentialStore.check(credential) | ||
) | ||
|
||
user <- { | ||
if (exists) | ||
SqlQueryT.fromQuery[ErrorOrResult, User]( | ||
store | ||
.getByLogin(credential.login) | ||
.map(_.toRight("The user doesn't exist")) | ||
) | ||
else | ||
SqlQueryT.liftF[ErrorOrResult, User](Left("Wrong credentials")) | ||
} | ||
} yield user | ||
|
||
runner(query).map { | ||
case Right(user) => | ||
NoContent.withSession("id" -> user.id.toString, "login" -> user.login) | ||
case Left(description) => BadRequest(description).withNewSession | ||
} | ||
} | ||
|
||
def logout: Action[AnyContent] = ConnectedAction { | ||
NoContent.withNewSession | ||
} | ||
|
||
} |
26 changes: 26 additions & 0 deletions
26
examples/todo-app/app/controller/model/AddTodoPayload.scala
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
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package com.zengularity.querymonad.examples.todoapp.controller.model | ||
|
||
import java.util.UUID | ||
|
||
import play.api.libs.json.{Json, Reads} | ||
|
||
import com.zengularity.querymonad.examples.todoapp.model.Todo | ||
|
||
case class AddTodoPayload( | ||
todoNumber: Int, | ||
content: String, | ||
done: Boolean | ||
) | ||
|
||
object AddTodoPayload { | ||
|
||
def toModel(payload: AddTodoPayload)(id: UUID, userId: UUID): Todo = Todo( | ||
id, | ||
payload.todoNumber, | ||
payload.content, | ||
userId, | ||
payload.done | ||
) | ||
|
||
implicit val format: Reads[AddTodoPayload] = Json.reads | ||
} |
Oops, something went wrong.