Skip to content

Commit

Permalink
Major refactoring, simplification and version update
Browse files Browse the repository at this point in the history
Play Framework 2.6.13
Mongo Scala driver 2.2.1
Sbt 0.13.15
Scala 2.11.8
  • Loading branch information
Ismet Ozozturk committed Apr 7, 2018
1 parent 282d9ce commit 6fa3c21
Show file tree
Hide file tree
Showing 36 changed files with 559 additions and 514 deletions.
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
language: scala

scala:
- 2.11.7
- 2.11.8

jdk:
- oraclejdk8
Expand Down
35 changes: 24 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,52 +1,65 @@
# Scalongo [![Build Status](https://travis-ci.org/iozozturk/scalongo.svg)](https://travis-ci.org/iozozturk/scalongo) [![works badge](https://cdn.rawgit.com/nikku/works-on-my-machine/v0.2.0/badge.svg)](https://github.com/nikku/works-on-my-machine)
# Scalongo [![Build Status](https://travis-ci.org/iozozturk/scalongo.svg)](https://travis-ci.org/iozozturk/scalongo) [![works badge](https://cdn.rawgit.com/nikku/works-on-my-machine/v0.2.0/badge.svg)](https://github.com/nikku/works-on-my-machine)


This is a sample seed project showcasing new scala driver for MongoDb on Play Framework 2.5.0
This is a sample seed project showcasing new scala driver for MongoDb on Play Framework 2.6.13

Best suitable for your REST backends, Single Page Application(SPA) backends

### What is this repository for? ###

* Bootstrap your Play Application with MongoDb
* Play Framework 2.5.0
* Play Framework 2.6.X
* No frontend implemented
* REST backend for all kind of your SPA or mobile applications
* Sample MailGun api used for automated emails for signup, reset pass etc.

### Implemented routines/endpoints ###

* signup
* activate account
* login
* logout
* forgot password
* set new password
* secure endpoint

### How do I get set up? ###

* Download the project
* Start your local MongoDb instance
* Start your local MongoDB instance
* Make changes at application.conf if necessary
* run "./activator run" command at root directory
* navigate to localhost:9000 on your browser
* in order for MailService to work fill in your MailGun apiKey in application.conf

### Sample Requests ###

* **POST** localhost:9000/signup
* **POST** localhost:9000/api/signup
* **Content-Type:**application/json
```
{
"username" : "ismet",
"password" : "123456",
"email" : "[email protected]",
"name" : "ismet"
}
```

* **POST** localhost:9000/login
* **POST** localhost:9000/api/login
* **Content-Type:**application/json
```
{
"username" : "ismet",
"email" : "ismet@ismet.com",
"password" : "123456"
}
```

* **GET** localhost:9000/secure
* **GET** localhost:9000/api/secure

* **POST** localhost:9000/logout
* **POST** localhost:9000/api/logout

### License ###

Copyright 2015 İsmet Özöztürk
Copyright 2018 İsmet Özöztürk

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
6 changes: 0 additions & 6 deletions activator.properties

This file was deleted.

18 changes: 10 additions & 8 deletions app/actions/SecureAction.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,32 @@ package actions

import com.google.inject.{Inject, Singleton}
import models.User
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.mvc._
import services.{SessionService, UserService}

import scala.concurrent.Future
import scala.concurrent.{ExecutionContext, Future}
import scala.util.control.NonFatal

/**
* Created by ismet on 21/12/15.
*/
class UserRequest[A](val user: User, val _session: models.Session, request: Request[A]) extends WrappedRequest[A](request) {
def sessionId = request.cookies.get("sessionId").get.value
def sessionId: String = request.cookies.get("sessionId").get.value
}

@Singleton
class SecureAction @Inject()(sessionService: SessionService,
userService: UserService) extends ActionBuilder[UserRequest] with ActionRefiner[Request, UserRequest] {
userService: UserService, val parser: BodyParsers.Default)
(implicit val executionContext: ExecutionContext)
extends ActionBuilder[UserRequest, AnyContent] with ActionRefiner[Request, UserRequest] {

override protected def refine[A](request: Request[A]): Future[Either[Result, UserRequest[A]]] = {

request.cookies.get("sessionId").map { c: Cookie =>
sessionService
.findUserAndSession(c.value)
.map(tuple => Right(new UserRequest[A](tuple._2, tuple._1, request)))
.map { tuple =>
if (tuple.isDefined)
Right(new UserRequest[A](tuple.get._2, tuple.get._1, request))
else Left(Results.Unauthorized)
}
.recover {
case NonFatal(_) => Left(Results.Unauthorized)
}
Expand Down
4 changes: 0 additions & 4 deletions app/bootstrap/InitSetup.scala
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,6 @@ import com.mongodb.client.model.Sorts._
import play.Logger
import repos.Mongo


/**
* Created by ismet on 16/12/15.
*/
class InitSetup @Inject()(mongo: Mongo) {

def preMessages(): Unit = {
Expand Down
3 changes: 0 additions & 3 deletions app/bootstrap/MongoModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,6 @@ package bootstrap

import com.google.inject.AbstractModule

/**
* Created by ismet on 16/12/15.
*/
class MongoModule extends AbstractModule {
protected def configure(): Unit = {
bind(classOf[InitSetup]).asEagerSingleton()
Expand Down
7 changes: 7 additions & 0 deletions app/common/AppLogger.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package common

import play.api.Logger

trait AppLogger {
protected lazy val logger = Logger(getClass.getName)
}
11 changes: 0 additions & 11 deletions app/common/ConvertibleToJson.scala

This file was deleted.

14 changes: 14 additions & 0 deletions app/common/JsonDecoders.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package common

import org.mongodb.scala.bson.ObjectId
import play.api.libs.json._

trait JsonDecoders {
implicit val objectIdWrites = new Writes[ObjectId] {
def writes(objId: ObjectId) = JsString(objId.toString)
}

implicit val objectIdReads = new Reads[ObjectId] {
override def reads(json: JsValue): JsResult[ObjectId] = JsSuccess(new ObjectId(json.as[String]))
}
}
2 changes: 1 addition & 1 deletion app/controllers/Application.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package controllers

import play.api.mvc._

class Application extends Controller {
class Application extends InjectedController {

def index = Action {
Ok
Expand Down
136 changes: 92 additions & 44 deletions app/controllers/AuthController.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,67 +5,65 @@ import java.util.UUID
import actions.SecureAction
import com.github.t3hnar.bcrypt._
import com.google.inject.Inject
import com.mongodb.MongoWriteException
import forms.AuthForms.{LoginData, SignupData}
import models.{Session, User}
import play.api.i18n.MessagesApi
import play.api.libs.concurrent.Execution.Implicits.defaultContext
import play.api.libs.json.{JsValue, Json}
import common.AppLogger
import common.JsonExtensions.ForJsValue
import mail.MailService
import play.api.libs.json.{JsObject, JsValue, Json}
import play.api.mvc._
import services.{SessionService, UserService}
import services.{PassResetService, SessionService, UserService}

import scala.concurrent.Future
import scala.concurrent.{ExecutionContext, Future}
import scala.util.{Failure, Success}

/**
* Created by ismet on 13/12/15.
*/
class AuthController @Inject()(userService: UserService,
sessionService: SessionService,
secureAction: SecureAction,
val messagesApi: MessagesApi) extends Controller {
mailService: MailService,
passResetService: PassResetService)
(implicit val executionContext: ExecutionContext) extends InjectedController with AppLogger {

def signup = Action.async { implicit request =>
val rawBody: JsValue = request.body.asJson.get
val signupData: SignupData = rawBody.validate[SignupData].get
val body: JsValue = request.body.asJson.get

val userObj = Json.obj(
"_id" -> UUID.randomUUID().toString,
"name" -> signupData.name,
"email" -> signupData.email,
"username" -> signupData.username,
"password" -> signupData.password.bcrypt,
"timestamp" -> System.currentTimeMillis())
val name = (body \ "name").as[String]
val email = (body \ "email").as[String]
val password = (body \ "password").as[String].bcrypt

val user = User(userObj)

userService.save(user).map((_) => {
userService.save(name, email, password).map((user) => {
passResetService.sendActivateAccountMail(user)
Ok
})
}

def login = Action.async { implicit request =>
val rawBody: JsValue = request.body.asJson.get
val loginData = rawBody.validate[LoginData].get

userService.findByUsername(loginData.username).map((user: User) => {
if (loginData.password.isBcrypted(user.password)) {
val sessionId: String = UUID.randomUUID().toString
val currentTimeMillis: Long = System.currentTimeMillis()
val session: Session = models.Session(Json.obj(
"_id" -> sessionId,
"userId" -> user._id,
"ip" -> request.remoteAddress,
"userAgent" -> request.headers.get("User-Agent").get,
"timestamp" -> currentTimeMillis,
"timeUpdate" -> currentTimeMillis
))
sessionService.save(session)
val response = Map("sessionId" -> sessionId)
Ok(Json.toJson(response)).withCookies(Cookie("sessionId", sessionId))
} else {
val body: JsValue = request.body.asJson.get.as[JsObject]
val email = body.getAs[String]("email")
val password = body.getAs[String]("password")

userService.findByEmail(email).map { user =>
if (user.isEmpty) {
logger.info(s"login blocked, reason=user_not_found, user=$email")
Unauthorized
} else {
if (!user.get.active) {
logger.info(s"login blocked, reason=inactive_account, user=$email")
Unauthorized
} else if (password.isBcrypted(user.get.password)) {
val sessionId: String = UUID.randomUUID().toString
sessionService.save(user.get._id, request.remoteAddress, request.headers.get("User-Agent").get, sessionId)
val responseObj = Json.obj("sessionId" -> sessionId, "user" -> user.get)
Ok(responseObj).withCookies(Cookie("sessionId", sessionId))
} else {
logger.info(s"login blocked, reason=wrong_pass, user=$email")
Unauthorized
}
}
})
}.recover {
case e =>
logger.info(s"login blocked, reason=${e.getMessage}")
e.printStackTrace()
Unauthorized
}
}

def securedSampleAction = secureAction { implicit request =>
Expand All @@ -77,5 +75,55 @@ class AuthController @Inject()(userService: UserService,
Ok.discardingCookies(DiscardingCookie("sessionId"))
}

def requestResetPassword(email: String) = Action { implicit request =>
userService.findByEmail(email) andThen {
case Success(user) =>
if (user.isDefined)
passResetService.save(email).map { resetObj =>
passResetService.sendResetRequestMail(user.get, resetObj)
}
else {
logger.error(s"user not found with email=$email")
}
case Failure(e) =>
logger.error(s"user not found with email:$email, message:${e.getMessage}")
e.printStackTrace()
}
Accepted
}

def resetPassword(id: String) = Action.async { implicit request =>
val body: JsValue = request.body.asJson.get.as[JsObject]
val password = body.getAs[String]("password")


passResetService.findIfNotUsed(id).flatMap { resetObj =>
if (resetObj.isDefined) {
passResetService.useResetRequest(id)
userService.findByEmail(resetObj.get.email).flatMap { user =>
passResetService.sendPassChangeMail(user.get, resetObj.get)
userService.changePasword(user.get._id, password).map(_ => Ok)
}
} else {
logger.error(s"reset password for resetId=$id not found")
Future {
Forbidden
}
}
}
}

def activateAccount(activationId: String) = Action.async { implicit request =>
userService.findByActivationId(activationId).map { user =>
if (user.isDefined) {
passResetService.sendUserActivatedMail(user.get)
userService.activateUser(user.get._id)
Ok
} else {
logger.info(s"user not found, activationId=$activationId")
NotFound
}
}
}

}
5 changes: 1 addition & 4 deletions app/filters/ExceptionFilter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,6 @@ import play.api.mvc._

import scala.concurrent.{ExecutionContext, Future}

/**
* Created by ismet on 16/12/15.
*/
class ExceptionFilter @Inject()(implicit val mat: Materializer, ec: ExecutionContext) extends Filter {

def apply(nextFilter: RequestHeader => Future[Result])
Expand All @@ -19,7 +16,7 @@ class ExceptionFilter @Inject()(implicit val mat: Materializer, ec: ExecutionCon
nextFilter(requestHeader).map { result =>
result
} transform (byPassResult, logError) recover {
case e: MongoWriteException => Results.Forbidden
case _: MongoWriteException => Results.Forbidden
case e: Exception => Results.InternalServerError(e.getMessage)
case _ => Results.InternalServerError
}
Expand Down
3 changes: 0 additions & 3 deletions app/filters/LoggingFilter.scala
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ import play.api.mvc._

import scala.concurrent.{ExecutionContext, Future}

/**
* Created by ismet on 16/12/15.
*/
class LoggingFilter @Inject() (implicit val mat: Materializer, ec: ExecutionContext) extends Filter {

def apply(nextFilter: RequestHeader => Future[Result])
Expand Down
Loading

0 comments on commit 6fa3c21

Please sign in to comment.