Browse Source

First commit

master
Nazar Kalinowski 6 months ago
commit
3a2dd8dc4b
54 changed files with 1297 additions and 0 deletions
  1. +16
    -0
      .gitignore
  2. +20
    -0
      app/Module.scala
  3. +32
    -0
      app/controllers/AdminController.scala
  4. +132
    -0
      app/controllers/AuthController.scala
  5. +85
    -0
      app/controllers/FilesController.scala
  6. +30
    -0
      app/controllers/HomeController.scala
  7. +39
    -0
      app/controllers/UserController.scala
  8. +30
    -0
      app/controllers/rest/RolesController.scala
  9. +44
    -0
      app/controllers/rest/UsersController.scala
  10. +16
    -0
      app/entity/ConfirmationCodes.scala
  11. +43
    -0
      app/entity/Roles.scala
  12. +16
    -0
      app/entity/UserRoles.scala
  13. +50
    -0
      app/entity/Users.scala
  14. +16
    -0
      app/forms/LoginForm.scala
  15. +17
    -0
      app/forms/RegisterForm.scala
  16. +14
    -0
      app/services/BCryptPasswordHasher.scala
  17. +11
    -0
      app/services/ConfirmationCodeService.scala
  18. +9
    -0
      app/services/ConfirmationCodeServiceImpl.scala
  19. +9
    -0
      app/services/EmailService.scala
  20. +17
    -0
      app/services/EmailServiceImpl.scala
  21. +15
    -0
      app/services/FileService.scala
  22. +13
    -0
      app/services/FileServiceImpl.scala
  23. +11
    -0
      app/services/FileUploader.scala
  24. +12
    -0
      app/services/FileUploaderImpl.scala
  25. +17
    -0
      app/services/PasswordHasher.scala
  26. +26
    -0
      app/services/dao/RoleDao.scala
  27. +69
    -0
      app/services/dao/SlickRoleDao.scala
  28. +79
    -0
      app/services/dao/SlickUserDao.scala
  29. +38
    -0
      app/services/dao/UserDao.scala
  30. +28
    -0
      app/util/Secure.scala
  31. +5
    -0
      app/views/about.scala.html
  32. +5
    -0
      app/views/admin.scala.html
  33. +14
    -0
      app/views/authorization.scala.html
  34. +14
    -0
      app/views/filenotfound.scala.html
  35. +44
    -0
      app/views/fileslist.scala.html
  36. +38
    -0
      app/views/generic/main.scala.html
  37. +14
    -0
      app/views/home.scala.html
  38. +7
    -0
      app/views/profile.scala.html
  39. +16
    -0
      app/views/registration.scala.html
  40. +14
    -0
      app/views/selfprofile.scala.html
  41. +26
    -0
      build.sbt
  42. +18
    -0
      conf/messages
  43. +18
    -0
      conf/messages.ru
  44. +35
    -0
      conf/routes
  45. +1
    -0
      project/build.properties
  46. +5
    -0
      project/plugins.sbt
  47. BIN
      public/images/favicon.png
  48. BIN
      public/images/logo.png
  49. +0
    -0
      public/javascripts/main.js
  50. +3
    -0
      public/stylesheets/filenotfound.css
  51. +6
    -0
      public/stylesheets/fileslist.css
  52. +44
    -0
      public/stylesheets/generic/main.css
  53. +6
    -0
      public/stylesheets/home.css
  54. +10
    -0
      test/Test1.scala

+ 16
- 0
.gitignore View File

@@ -0,0 +1,16 @@
project/project
project/target
target
tmp
.history
dist
/.idea
/*.iml
/out
/.idea_modules
/.classpath
/.project
/RUNNING_PID
/.settings
conf/application.conf
conf/logback.xml

+ 20
- 0
app/Module.scala View File

@@ -0,0 +1,20 @@
import com.google.inject.AbstractModule
import at.favre.lib.crypto.bcrypt.BCrypt

/**
* This class is a Guice module that tells Guice how to bind several
* different types. This Guice module is created when the Play
* application starts.

* Play will automatically use any class called `Module` that is in
* the root package. You can create modules in other locations by
* adding `play.modules.enabled` settings to the `application.conf`
* configuration file.
*/
class Module extends AbstractModule {

override def configure() = {
bind(classOf[BCrypt.Hasher]).toInstance(BCrypt.withDefaults())
bind(classOf[BCrypt.Verifyer]).toInstance(BCrypt.verifyer())
}
}

+ 32
- 0
app/controllers/AdminController.scala View File

@@ -0,0 +1,32 @@
package controllers

import javax.inject.{Inject, Singleton}
import play.api.i18n.{I18nSupport, Messages}
import play.api.mvc._
import services.dao.{RoleDao, UserDao}
import util.Secure

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

@Singleton
class AdminController @Inject()(val cc: ControllerComponents,
implicit val userDao: UserDao[Future],
implicit val roleDao: RoleDao[Future])
extends AbstractController(cc)
with Secure with I18nSupport {

def admin = Action.async { implicit request: Request[AnyContent] =>
getUser.flatMap {
case Some(user) =>
roleDao.getUserRoles(user.id).map(roles => {
if (roles.exists(_.name.equals("administrator"))) {
Ok(views.html.admin(getUsername.get))
}
else Redirect(routes.HomeController.home())
})
case None =>
Future(Redirect(routes.AuthController.authorization()))
}
}
}

+ 132
- 0
app/controllers/AuthController.scala View File

@@ -0,0 +1,132 @@
package controllers

import entity.User

import scala.concurrent.ExecutionContext.Implicits.global
import forms.{LoginForm, RegisterForm}
import javax.inject._
import org.slf4j.{Logger, LoggerFactory}
import play.api.i18n.{I18nSupport, Messages}
import play.api.mvc._
import services.{ConfirmationCodeService, EmailService, PasswordHasher}
import services.dao.{RoleDao, UserDao}
import util.Secure

import scala.collection.mutable
import scala.concurrent.Future
import scala.util.{Failure, Success}

@Singleton
class AuthController @Inject()(cc: ControllerComponents,
encryptor: PasswordHasher,
codeGenerator: ConfirmationCodeService,
val emailer: EmailService,
userDao: UserDao[Future],
roleDao: RoleDao[Future])
extends AbstractController(cc)
with Secure with I18nSupport {

val logger: Logger = LoggerFactory.getLogger(getClass)

val usedEmails: mutable.Set[String] = mutable.Set[String]()
val usedAddresses: mutable.Set[String] = mutable.Set[String]()

def registration(): Action[AnyContent] = Action { implicit request: Request[AnyContent] =>
Ok(views.html.registration(RegisterForm.form))
}

private def getConfirmationLink(confirmationCode: String)(implicit request: Request[AnyContent]): String =
s"http${if (request.secure) "s" else ""}://${request.host}/confirm/$confirmationCode"

def register = Action.async { implicit request: Request[AnyContent] =>
RegisterForm.form.bindFromRequest.fold(
formWithErrors => {
logger.debug(s"Registration form has errors")
Future(BadRequest(views.html.registration(formWithErrors)))
},
userData => {
val RegisterForm.Data(name, email, password) = userData
val address = request.connection.remoteAddressString
if (usedEmails(email)) {
Future(BadRequest("This email address has already been used for registration!"))
} else if (usedAddresses(address)) {
Future(BadRequest("Your ip address has already been used for registration!"))
} else {
val optExistingUser = for {
byName <- userDao.getByName(name)
byEmail <- userDao.getByEmail(email)
} yield byName.orElse(byEmail)
optExistingUser.flatMap {
case Some(_) =>
logger.debug(s"Duplicated user tried to register! Email: '$email' nickname: '$name'")
Future(BadRequest(views.html.registration(RegisterForm.form.fill(userData))))
case None =>
userDao.create(User(0, name, email, encryptor.encrypt(password))).
flatMap(_ => userDao.getIdByName(name).map(_.get)).
map(userId => {
val confirmationCode = codeGenerator.generateConfirmationCode
userDao.setConfirmationCode(userId, confirmationCode)
confirmationCode
}).
transformWith {
case Failure(e) =>
logger.warn("Failed to register a user!", e)
Future(BadRequest(views.html.registration(RegisterForm.form.fill(userData))))
case Success(confirmationCode) =>
emailer.send(email, "Registration on GWMDevelopments",
"If you haven't recently registered on GWMDevelopments you can safely ignore this message.\n" +
s"Confirmation link: ${getConfirmationLink(confirmationCode)}")
usedEmails += email
usedAddresses += address
Future(Redirect(routes.HomeController.home(), SEE_OTHER))
}
}
}
}
)
}

def authorization() = Action { implicit request: Request[AnyContent] =>
Ok(views.html.authorization(LoginForm.form))
}

def login() = Action.async { implicit request: Request[AnyContent] =>
LoginForm.form.bindFromRequest.fold(
formWithErrors => {
Future(BadRequest(views.html.authorization(formWithErrors)))
},
userData => userDao.getByLogin(userData.login).map(option => {
val redirectFail = Redirect(routes.AuthController.authorization(), SEE_OTHER).
flashing("error" -> "Wrong user or password")
option.map(user => {
if (encryptor.check(userData.password, user.password)) {
Redirect(routes.HomeController.home(), SEE_OTHER).
withSession("username" -> user.name)
} else redirectFail
}).getOrElse(redirectFail)
})
)
}

def confirmRegistration(confirmationCode: String) = Action.async {
userDao.getByConfirmationCode(confirmationCode).
flatMap {
case Some(user) =>
usedEmails -= user.email
userDao.deleteConfirmationCode(user.id).
flatMap(_ => userDao.confirm(user.id)).
transformWith {
case Failure(e) =>
logger.warn("Failed to confirm user!", e)
Future(Ok("Something bad has happened!"))
case Success(_) =>
Future(Ok("Successfully confirmed!"))
}
case None => Future(BadRequest("No confirmationCode like that exists"))
}
}

def logout = Action {
Redirect("/").withNewSession
}
}

+ 85
- 0
app/controllers/FilesController.scala View File

@@ -0,0 +1,85 @@
package controllers

import java.nio.file.Paths

import entity.{Role, Roles, User}
import javax.inject._
import play.api.i18n.{I18nSupport, Messages}
import play.api.mvc._
import services.FileService
import services.dao.{RoleDao, UserDao}
import util.Secure

import scala.concurrent.{ExecutionContext, ExecutionContextExecutor, Future}

object FilesController {

val kilobyte: Double = 1024.0
val megabyte: Double = kilobyte * kilobyte
val gigabyte: Double = megabyte * kilobyte
val petabyte: Double = gigabyte * kilobyte

def sizeToString(size: Long): String = {
if (size > petabyte) "%.2f PB".format(size / petabyte)
else if (size > gigabyte) "%.2f GB".format(size / gigabyte)
else if (size > megabyte) "%.2f MB".format(size / megabyte)
else if (size > kilobyte) "%.2f KB".format(size / kilobyte)
else s"$size B"
}
}

@Singleton
class FilesController @Inject()(implicit val userDao: UserDao[Future],
implicit val roleDao: RoleDao[Future],
fileService: FileService,
cc: ControllerComponents)
extends AbstractController(cc)
with I18nSupport
with Secure {

implicit val ec: ExecutionContextExecutor = ExecutionContext.global

def files(name: String) = Action.async { implicit request: Request[AnyContent] =>
isAdministrator.map { showUpload =>
val path = fileService.publicFilesDirectory/name
if (path.exists) {
if (path.isFile) {
Ok.sendFile(path.jfile)
} else {
Ok(views.html.fileslist(fileService.publicFilesDirectory, path.toDirectory, showUpload.getOrElse(false)))
}
} else {
BadRequest(views.html.filenotfound(name))
}
}
}

def upload() = Action.async(parse.multipartFormData) { implicit request =>
request.body.
file("fileToUpload").
map { file =>
isAdministrator.map {
case Some(true) =>
val dataParts = request.body.dataParts
if (!dataParts.isDefinedAt("path")) {
//return BadRequest("")
}
val dataPartsPath = request.body.dataParts("path").headOption
if (dataPartsPath.isEmpty) {
//return BadRequest("")
}
val path = (fileService.publicFilesDirectory/dataPartsPath.head).toString
val fileName = Paths.get(file.filename).getFileName.toString
file.ref.copyTo(Paths.get(path, fileName), replace = true)
Ok("File uploaded")
case Some(false) =>
Redirect(routes.FilesController.files("")).
flashing("error" -> "You have no permission to upload files")
case None =>
Redirect(routes.FilesController.files("")).
flashing("error" -> "You need to be authorised to upload files")
}
}.getOrElse(Future(Redirect(routes.FilesController.files("")).
flashing("error" -> "Missing a file to upload")))
}
}

+ 30
- 0
app/controllers/HomeController.scala View File

@@ -0,0 +1,30 @@
package controllers

import javax.inject._
import play.api.i18n.I18nSupport
import play.api.mvc._
import services.FileService
import services.dao.{RoleDao, UserDao}
import util.Secure
import scala.concurrent.ExecutionContext.Implicits._

import scala.concurrent.Future

@Singleton
class HomeController @Inject()(val fileService: FileService,
val cc: ControllerComponents,
implicit val userDao: UserDao[Future],
implicit val roleDao: RoleDao[Future])
extends AbstractController(cc)
with Secure with I18nSupport {

def home = Action.async { implicit request: Request[AnyContent] =>
isAdministrator.map { admin =>
Ok(views.html.home(admin.getOrElse(false)))
}
}

def about = Action { implicit request: Request[AnyContent] =>
Ok(views.html.about())
}
}

+ 39
- 0
app/controllers/UserController.scala View File

@@ -0,0 +1,39 @@
package controllers

import entity.User
import entity.Role
import javax.inject.Inject
import play.api.i18n.I18nSupport
import play.api.mvc.{AbstractController, AnyContent, ControllerComponents, Request}
import services.dao.{RoleDao, UserDao}
import util.Secure

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

class UserController @Inject()(val cc: ControllerComponents,
implicit val userDao: UserDao[Future],
implicit val roleDao: RoleDao[Future])
extends AbstractController(cc)
with Secure with I18nSupport {

def selfProfile = Action.async { implicit request: Request[AnyContent] =>
getUser.flatMap {
case Some(user) => roleDao.getUserRoles(user.id).map { roles =>
Ok(views.html.selfprofile(user, roles))
}
case None => Future(BadRequest("You are not authorised, go away!"))
}
}

def userProfileById(id: Long) = userProfile(id.toString, userDao.getById(id))

def userProfileByName(name: String) = userProfile(name, userDao.getByName(name))

private def userProfile(query: String, userF: Future[Option[User]]) = Action.async { implicit request: Request[AnyContent] =>
userF.map {
case Some(user) => Ok(views.html.profile(user))
case None => Ok(s"User $query does not exist, go away!")
}
}
}

+ 30
- 0
app/controllers/rest/RolesController.scala View File

@@ -0,0 +1,30 @@
package controllers.rest

import javax.inject.{Inject, Singleton}
import play.api.libs.json.Json
import entity.RolesToJson.writesRole
import play.api.mvc.{AbstractController, AnyContent, ControllerComponents, Request}
import services.dao.RoleDao

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

@Singleton
class RolesController @Inject()(cc: ControllerComponents,
roleDao: RoleDao[Future])
extends AbstractController(cc) {

def roles = Action.async { implicit request: Request[AnyContent] =>
roleDao.getRoles.map(seq => Ok(Json.toJson(seq)))
}

def role(id: Long) = Action.async { implicit request: Request[AnyContent] =>
roleDao.getOptionById(id).map(optRole => {
if (optRole.isDefined) {
Ok(Json.toJson(optRole.get))
} else {
BadRequest("No role with id " + id)
}
})
}
}

+ 44
- 0
app/controllers/rest/UsersController.scala View File

@@ -0,0 +1,44 @@
package controllers.rest

import entity.UsersToJson.writesUser
import entity.UsersToJson.writesUserWithRoles
import javax.inject.{Inject, Singleton}
import play.api.libs.json.Json
import play.api.mvc._
import services.dao.{RoleDao, UserDao}

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future

@Singleton
class UsersController @Inject()(cc: ControllerComponents,
userDao: UserDao[Future],
roleDao: RoleDao[Future])
extends AbstractController(cc) {

def users() = Action.async { implicit request: Request[AnyContent] =>
userDao.getUsers.map(seq => Ok(Json.toJson(seq)))
}

def user(id: Long, withRoles: Boolean) = Action.async { implicit request: Request[AnyContent] =>
if (withRoles) {
userDao.getById(id).zip(roleDao.getUserRoles(id)).map(tuple => {
val optUser = tuple._1
val roles = tuple._2
if (optUser.isDefined) {
Ok(Json.toJson((optUser.get, roles)))
} else {
BadRequest("No user with id " + id)
}
})
} else {
userDao.getById(id).map(optUser => {
if (optUser.isDefined) {
Ok(Json.toJson(optUser.get))
} else {
BadRequest("No user with id " + id)
}
})
}
}
}

+ 16
- 0
app/entity/ConfirmationCodes.scala View File

@@ -0,0 +1,16 @@
package entity

import slick.jdbc.MySQLProfile.api._
import slick.lifted.{TableQuery, Tag}

object ConfirmationCodes {

val confirmationCodes = TableQuery[ConfirmationCodes]
}

class ConfirmationCodes(tag: Tag) extends Table[(Long, String)](tag, "confirmation_codes") {

def userId = column[Long]("userId")
def confirmationCode = column[String]("confirmationCode", O.Length(24))
def * = (userId, confirmationCode)
}

+ 43
- 0
app/entity/Roles.scala View File

@@ -0,0 +1,43 @@
package entity

import play.api.libs.json.{Json, Writes}
import slick.jdbc.MySQLProfile.api._
import slick.lifted.TableQuery

object RolesToJson {

implicit val writesRole: Writes[Role] = Writes[Role] {
case Role(id, name, color, priority) =>
Json.obj(
"id" -> id,
"name" -> name,
"color" -> color,
"priority" -> priority
)
}
}

case class Role(id: Long, name: String, color: Int, priority: Int)

object DefaultRoles {

object PremiumUser extends Role(0, "premium", 100, 200)
object Moderator extends Role(0, "moderator", 123, 900)
object Administrator extends Role(0, "administrator", 900, 1000)

val list: List[Role] = List(PremiumUser, Moderator, Administrator)
}

object Roles {

val roles = TableQuery[Roles]
}

class Roles(tag: Tag) extends Table[Role](tag, "roles") {

def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name", O.Unique, O.Length(40))
def color = column[Int]("color")
def priority = column[Int]("priority", O.Unique)
def * = (id, name, color, priority) <> (Role.tupled, Role.unapply)
}

+ 16
- 0
app/entity/UserRoles.scala View File

@@ -0,0 +1,16 @@
package entity

import slick.lifted.{TableQuery, Tag}
import slick.jdbc.MySQLProfile.api._

object UserRoles {

val userRoles = TableQuery[UserRoles]
}

class UserRoles(tag: Tag) extends Table[(Long, Long)](tag, "user_roles") {

def userId = column[Long]("userId")
def roleId = column[Long]("roleId")
def * = (userId, roleId)
}

+ 50
- 0
app/entity/Users.scala View File

@@ -0,0 +1,50 @@
package entity

import play.api.libs.json.{Json, Writes}
import slick.jdbc.MySQLProfile.api._
import slick.lifted.TableQuery

object UsersToJson {

implicit val writesUser: Writes[User] = Writes[User] {
case User(id, name, email, _, _, _) =>
Json.obj(
"id" -> id,
"name" -> name,
"email" -> email
)
}

implicit val writesUserWithRoles: Writes[(User, Seq[Role])] = Writes[(User, Seq[Role])] {
case (User(id, name, email, _, _, _), roles) =>
Json.obj(
"id" -> id,
"name" -> name,
"email" -> email,
"roles" -> roles.map(role => Json.obj(
"id" -> role.id,
"name" -> role.name,
"color" -> role.color,
"priority" -> role.priority
))
)
}
}

case class User(id: Long, name: String, email: String, password: Array[Byte], registerDate: Long = System.currentTimeMillis(), confirmed: Boolean = false)

object Users {

val users = TableQuery[Users]
}

class Users(tag: Tag) extends Table[User](tag, "users") {

def id = column[Long]("id", O.PrimaryKey, O.AutoInc)
def name = column[String]("name", O.Unique, O.Length(40))
def email = column[String]("email", O.Unique, O.Length(40))
def password = column[Array[Byte]]("password", O.Length(60))
def registerDate = column[Long]("registerDate")
def confirmed = column[Boolean]("confirmed")
def * = (id, name, email, password, registerDate, confirmed) <> (User.tupled, User.unapply)
}

+ 16
- 0
app/forms/LoginForm.scala View File

@@ -0,0 +1,16 @@
package forms

object LoginForm {

import play.api.data.Forms._
import play.api.data.Form

case class Data(login: String, password: String)

val form: Form[Data] = Form(
mapping(
"login" -> nonEmptyText(minLength = 3),
"password" -> nonEmptyText(minLength = 8)
)(Data.apply)(Data.unapply)
)
}

+ 17
- 0
app/forms/RegisterForm.scala View File

@@ -0,0 +1,17 @@
package forms

object RegisterForm {

import play.api.data.Forms._
import play.api.data.Form

case class Data(name: String, email: String, password: String)

val form: Form[Data] = Form(
mapping(
"name" -> nonEmptyText(minLength = 3),
"email" -> email,
"password" -> nonEmptyText(minLength = 8)
)(Data.apply)(Data.unapply)
)
}

+ 14
- 0
app/services/BCryptPasswordHasher.scala View File

@@ -0,0 +1,14 @@
package services

import at.favre.lib.crypto.bcrypt.BCrypt
import javax.inject.Inject

class BCryptPasswordHasher @Inject()(val hasher: BCrypt.Hasher, val verifyer: BCrypt.Verifyer)
extends PasswordHasher {

override def encrypt(password: Array[Byte]): Array[Byte] =
hasher.hash(12, password)

override def check(password: Array[Byte], hash: Array[Byte]): Boolean =
verifyer.verify(password, hash).verified
}

+ 11
- 0
app/services/ConfirmationCodeService.scala View File

@@ -0,0 +1,11 @@
package services

import com.google.inject.ImplementedBy

@ImplementedBy(classOf[ConfirmationCodeServiceImpl])
trait ConfirmationCodeService {

def generateConfirmationCode(length: Int): String

def generateConfirmationCode: String = generateConfirmationCode(24)
}

+ 9
- 0
app/services/ConfirmationCodeServiceImpl.scala View File

@@ -0,0 +1,9 @@
package services

import scala.util.Random

class ConfirmationCodeServiceImpl extends ConfirmationCodeService {

override def generateConfirmationCode(length: Int): String =
Random.alphanumeric.take(length).foldLeft("")(_ + _)
}

+ 9
- 0
app/services/EmailService.scala View File

@@ -0,0 +1,9 @@
package services

import com.google.inject.ImplementedBy

@ImplementedBy(classOf[EmailServiceImpl])
trait EmailService {

def send(to: String, subject: String, body: String): Unit
}

+ 17
- 0
app/services/EmailServiceImpl.scala View File

@@ -0,0 +1,17 @@
package services

import javax.inject.Inject
import play.api.libs.mailer._

class EmailServiceImpl @Inject()(mailerClient: MailerClient) extends EmailService {

override def send(to: String, subject: String, body: String): Unit = {
val email = Email(
subject,
"GWMDevelopments <gwm@gwm.dev>",
Seq(to),
bodyText = Some(body),
)
mailerClient.send(email)
}
}

+ 15
- 0
app/services/FileService.scala View File

@@ -0,0 +1,15 @@
package services

import com.google.inject.ImplementedBy

import scala.reflect.io.Directory

@ImplementedBy(classOf[FileServiceImpl])
trait FileService {

def publicFilesDirectory: Directory

def usersDirectory: Directory

def userDirectory(id: Long): Directory = usersDirectory/Directory(id.toString)
}

+ 13
- 0
app/services/FileServiceImpl.scala View File

@@ -0,0 +1,13 @@
package services

import com.typesafe.config.Config
import javax.inject.Inject

import scala.reflect.io.Directory

class FileServiceImpl @Inject()(config: Config) extends FileService {

override def publicFilesDirectory: Directory = Directory(config.getString("publicFilesDirectory"))

override def usersDirectory: Directory = Directory(config.getString("usersDirectory"))
}

+ 11
- 0
app/services/FileUploader.scala View File

@@ -0,0 +1,11 @@
package services

import com.google.inject.ImplementedBy
import play.api.libs.Files
import play.api.mvc.MultipartFormData

@ImplementedBy(classOf[FileUploaderImpl])
trait FileUploader {

def uploadFile(file: MultipartFormData.FilePart[Files.TemporaryFile]): Unit
}

+ 12
- 0
app/services/FileUploaderImpl.scala View File

@@ -0,0 +1,12 @@
package services

import javax.inject.Inject
import play.api.libs.Files
import play.api.mvc.MultipartFormData

class FileUploaderImpl @Inject()(fileService: FileService) extends FileUploader {

override def uploadFile(file: MultipartFormData.FilePart[Files.TemporaryFile]): Unit = {

}
}

+ 17
- 0
app/services/PasswordHasher.scala View File

@@ -0,0 +1,17 @@
package services

import com.google.inject.ImplementedBy

@ImplementedBy(classOf[BCryptPasswordHasher])
trait PasswordHasher {

def encrypt(password: Array[Byte]): Array[Byte]

def encrypt(password: String): Array[Byte] =
encrypt(password.getBytes())

def check(password: Array[Byte], hash: Array[Byte]): Boolean

def check(password: String, hash: Array[Byte]): Boolean =
check(password.getBytes(), hash)
}

+ 26
- 0
app/services/dao/RoleDao.scala View File

@@ -0,0 +1,26 @@
package services.dao

import com.google.inject.ImplementedBy
import entity.{Role, User}

@ImplementedBy(classOf[SlickRoleDao])
trait RoleDao[F[_]] {

def getRoles: F[Seq[Role]]

def getOptionById(id: Long): F[Option[Role]]

def getById(id: Long): F[Role]

def getOptionByName(name: String): F[Option[Role]]

def getByName(name: String): F[Role]

def create(role: Role): F[Int]

def delete(id: Long): F[Int]

def getUserRoles(userId: Long): F[Seq[Role]]

def getRoleUsers(roleId: Long): F[Seq[User]]
}

+ 69
- 0
app/services/dao/SlickRoleDao.scala View File

@@ -0,0 +1,69 @@
package services.dao

import entity.{DefaultRoles, Role, User}
import entity.Roles.roles
import entity.Users.users
import entity.UserRoles.userRoles
import javax.inject.{Inject, Singleton}
import play.api.Logging
import play.api.db.slick.DatabaseConfigProvider
import slick.jdbc.MySQLProfile.api._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.util.Success
import scala.util.Failure

@Singleton
class SlickRoleDao @Inject()(val dbConfig: DatabaseConfigProvider) extends RoleDao[Future] with Logging {

private val db = dbConfig.get.db

db.run(DBIO.seq(
roles.schema.createIfNotExists,
userRoles.schema.createIfNotExists)).
onComplete {
case Success(_) => logger.info("Startup db queries successfully executed.")
case Failure(e) => logger.error("Startup db queries failed!", e)
}

DefaultRoles.list.foreach(role => db.run(roles += role).onComplete {
case Success(_) => logger.info(s"Role $role successfully inserted into the db")
case Failure(e) => logger.debug(s"Failed to insert role $role into the db (probably it has already been inserted)", e)
})

override def getRoles: Future[Seq[Role]] = db.run(roles.result)

override def getOptionById(id: Long): Future[Option[Role]] = db.run(roles.filter(_.id === id).result.headOption)

override def getById(id: Long): Future[Role] = db.run(roles.filter(_.id === id).result.head)

override def getOptionByName(name: String): Future[Option[Role]] = db.run(roles.filter(_.name === name).result.headOption)

override def getByName(name: String): Future[Role] = db.run(roles.filter(_.name === name).result.head)

override def create(role: Role): Future[Int] = db.run(roles += role)

override def delete(id: Long): Future[Int] = db.run(roles.filter(_.id === id).delete)

override def getUserRoles(userId: Long): Future[Seq[Role]] = {
val result = db.run(roles.
join(userRoles.filter(_.userId === userId)). //Select roles of the user
on(_.id === _.roleId). //Select roles from the db
map(_._1).result) //Take only roles
result.onComplete {
case Failure(exception) => exception.printStackTrace()
case Success(value) => {
println(s"size: ${value.size}")
value.foreach(role => s"selected role: ${role.name}")
}
}
result
}

override def getRoleUsers(roleId: Long): Future[Seq[User]] =
db.run(users.
join(userRoles.filter(_.roleId === roleId)).
on(_.id === _.userId).
map(_._1).result)
}

+ 79
- 0
app/services/dao/SlickUserDao.scala View File

@@ -0,0 +1,79 @@
package services.dao

import entity.ConfirmationCodes.confirmationCodes
import entity.User
import entity.Users.users
import javax.inject.{Inject, Singleton}
import play.api.Logging
import play.api.db.slick.DatabaseConfigProvider
import slick.jdbc.MySQLProfile.api._

import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.Future
import scala.language.higherKinds
import scala.util.{Failure, Success}

@Singleton
class SlickUserDao @Inject()(val dbConfig: DatabaseConfigProvider) extends UserDao[Future] with Logging {

private val db = dbConfig.get.db

db.run(DBIO.seq(
users.schema.createIfNotExists,
confirmationCodes.schema.createIfNotExists)).
onComplete {
case Success(_) => logger.info("Startup db queries successfully executed.")
case Failure(e) => logger.error("Startup db queries failed!", e)
}

override def getUsers: Future[Seq[User]] =
db.run(users.result)

override def getById(id: Long): Future[Option[User]] =
db.run(users.filter(_.id === id).result.headOption)

override def getIdByName(name: String): Future[Option[Long]] =
db.run(users.filter(_.name === name).map(_.id).result.headOption)

override def getIdByEmail(email: String): Future[Option[Long]] =
db.run(users.filter(_.email === email).map(_.id).result.headOption)

override def getByName(name: String): Future[Option[User]] =
db.run(users.filter(_.name === name).result.headOption)

override def getByEmail(email: String): Future[Option[User]] =
db.run(users.filter(_.email === email).result.headOption)

override def getByLogin(login: String): Future[Option[User]] =
getByName(login).flatMap {
case user @ Some(_) => Future(user)
case None => getByEmail(login)
}

override def setConfirmationCode(id: Long, confirmationCode: String): Future[Int] =
db.run(confirmationCodes += (id, confirmationCode))

override def getConfirmationCode(id: Long): Future[Option[String]] =
db.run(confirmationCodes.filter(_.userId === id).map(_.confirmationCode).result.headOption)

def getIdByConfirmationCode(confirmationCode: String): Future[Option[Long]] =
db.run(confirmationCodes.filter(_.confirmationCode === confirmationCode).map(_.userId).result.headOption)

def getByConfirmationCode(confirmationCode: String): Future[Option[User]] =
getIdByConfirmationCode(confirmationCode).flatMap {
case Some(id) => getById(id)
case None => Future(None)
}

override def deleteConfirmationCode(id: Long): Future[Int] =
db.run(confirmationCodes.filter(_.userId === id).delete)

override def confirm(id: Long): Future[Int] =
db.run(users.filter(_.id === id).map(_.confirmed).update(true))

override def create(user: User): Future[Int] =
db.run(users += user)

override def delete(id: Long): Future[Int] =
db.run(users.filter(_.id === id).delete)
}

+ 38
- 0
app/services/dao/UserDao.scala View File

@@ -0,0 +1,38 @@
package services.dao

import com.google.inject.ImplementedBy
import entity.User

@ImplementedBy(classOf[SlickUserDao])
trait UserDao[F[_]] {

def getUsers: F[Seq[User]]

def getById(id: Long): F[Option[User]]

def getIdByName(name: String): F[Option[Long]]

def getIdByEmail(email: String): F[Option[Long]]

def getByName(name: String): F[Option[User]]

def getByEmail(email: String): F[Option[User]]

def getByLogin(login: String): F[Option[User]]

def setConfirmationCode(id: Long, confirmationCode: String): F[Int]

def getConfirmationCode(id: Long): F[Option[String]]

def getIdByConfirmationCode(confirmationCode: String): F[Option[Long]]

def getByConfirmationCode(confirmationCode: String): F[Option[User]]

def deleteConfirmationCode(id: Long): F[Int]

def confirm(id: Long): F[Int]

def create(user: User): F[Int]

def delete(id: Long): F[Int]
}

+ 28
- 0
app/util/Secure.scala View File

@@ -0,0 +1,28 @@
package util

import entity.User
import play.api.mvc._
import services.dao.{RoleDao, UserDao}

import scala.concurrent.ExecutionContext.Implicits._
import scala.concurrent.Future

trait Secure {

implicit def getUsername[A](implicit request: Request[A]): Option[String] =
request.session.get("username")

implicit def getUser[A](implicit request: Request[A], userDao: UserDao[Future]): Future[Option[User]] =
getUsername match {
case Some(name) => userDao.getByName(name)
case None => Future(None)
}

def isAdministrator[A](implicit request: Request[A], userDao: UserDao[Future], roleDao: RoleDao[Future]): Future[Option[Boolean]] =
getUser.flatMap {
case Some(user) =>
roleDao.getUserRoles(user.id).map(roles => Some(roles.exists(_.name.equals("administrator"))))
case None =>
Future(None)
}
}

+ 5
- 0
app/views/about.scala.html View File

@@ -0,0 +1,5 @@
@()(implicit messagesProvider: MessagesProvider, optUsername: Option[String] = None)

@generic.main(messagesProvider.messages("about.title")) {
<span>TODO: some info about me</span>
}

+ 5
- 0
app/views/admin.scala.html View File

@@ -0,0 +1,5 @@
@(optUser: String)(implicit messagesProvider: MessagesProvider, optUsername: Option[String] = None)

@generic.main(messagesProvider.messages("index.title")) {
<h1>Admin's page!</h1>
}

+ 14
- 0
app/views/authorization.scala.html View File

@@ -0,0 +1,14 @@
@import helper._
@import forms.LoginForm

@(userForm: Form[LoginForm.Data])(implicit request: RequestHeader, messagesProvider: MessagesProvider, optUsername: Option[String] = None)

@generic.main(messagesProvider.messages("login.title")) {
<h1><b>Sign-in</b></h1>
@helper.form(action = routes.AuthController.login()) {
@CSRF.formField
@helper.inputText(userForm("login"))
@helper.inputPassword(userForm("password"))
<input type="submit">
}
}

+ 14
- 0
app/views/filenotfound.scala.html View File

@@ -0,0 +1,14 @@
@(file: String)(implicit messagesProvider: MessagesProvider, optUsername: Option[String] = None)

<!DOCTYPE html>
<html lang="en">
<head>
<title>GWMDevelopments - file not found</title>
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/main.css")">
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.ico")">
</head>
<body>
<h1>File @file not found!</h1>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
</body>
</html>

+ 44
- 0
app/views/fileslist.scala.html View File

@@ -0,0 +1,44 @@
@import controllers.FilesController
@import helper._
@import scala.reflect.io.Directory
@import scala.reflect.io.Path

@(root: Directory, file: Directory, showUpload: Boolean)(implicit request: RequestHeader, messagesProvider: MessagesProvider, optUsername: Option[String] = None)

@fileToHref(root: Directory, path: Path) = @{
val relativized = root.relativize(path).toString
if (relativized.isEmpty) "/files"
else s"/files/$relativized"
}

@generic.main(messagesProvider.messages("fileslist.title")) {
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/fileslist.css")">
@if(showUpload){
@helper.form(action = routes.FilesController.upload, Symbol("enctype") -> "multipart/form-data") {
@CSRF.formField
<input type="file" name="fileToUpload"><br>
Custom name (optional): <input type="text" name="customName"><br>
<input type="hidden" name="path" value="@root.relativize(file).toString"/>
<input type="submit" value="Upload">
}
}
<hr>
<table border="1" width="100%">
<caption><b>Files</b></caption>
@if(!root.equals(file)){
<tr>
<td colspan="3"><a href="@fileToHref(root, file.parent)"><b>..</b></a></td>
</tr>
}
@for(subFile <- file.list){
<tr>
<td><a href="@fileToHref(root, subFile)"><b>@subFile.name</b> @if(subFile.isFile){
<i> (@FilesController.sizeToString(subFile.length))</i>
}</a></td>
<td><input/><button>Rename</button></td>
<td><button>Delete</button></td>
</tr>
}
</table>
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
}

+ 38
- 0
app/views/generic/main.scala.html View File

@@ -0,0 +1,38 @@
@(title: String)(content: Html)(implicit messagesProvider: MessagesProvider, optUsername: Option[String] = None)

<!doctype html>
<html>
<head>
<title>@title</title>
<link rel="shortcut icon" type="image/png" href="@routes.Assets.versioned("images/favicon.png")">
<link rel="stylesheet" media="screen" href="@routes.Assets.versioned("stylesheets/generic/main.css")">
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/npm/toastify-js/src/toastify.min.css">
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/toastify-js"></script>
</head>
<body>
<div class="wrapper">
<div class="header">
<div class="spasite-menya-zastivili-delat-frontend">
<a class="logo-img" href="/"><img class="logo" src="@routes.Assets.versioned("images/logo.png")"></a>
<b>GWMDevelopments</b>
</div>
<div class="user-info">
@if(optUsername.isDefined) {
<span>Hello, <a href="/profile">@optUsername.get</a>!</span><br>
<a href="/logout">@messagesProvider.messages("index.logout")</a>
} else {
<a href="/registration">@messagesProvider.messages("index.registration")</a><br>
<a href="/authorization">@messagesProvider.messages("index.login")</a>
}
</div>
</div>
<div class="content">
@content
</div>
<div class="footer">
<span>This site is licensed under <a href="https://tldrlegal.com/license/gnu-affero-general-public-license-v3-(agpl-3.0)">GNU AGPL v3</a></span><br>
<span>Click <a href="https:/gitea.gwm.dev/GWM/GWMDevelopmentsSite">here</a> to view or download the source code</span>
</div>
</div>
</body>
</html>

+ 14
- 0
app/views/home.scala.html View File

@@ -0,0 +1,14 @@
@(admin: Boolean)(implicit messagesProvider: MessagesProvider, optUsername: Option[String] = None)

@generic.main(messagesProvider.messages("index.title")) {
<h1><b><font color="#FB4C19">GWMDevelopments</font> site.</b></h1>
<h2><font color="red">TODO: make TODO</font></h2>
<a href="/files">@messagesProvider.messages("index.files")</a><br>
<a href="/about">@messagesProvider.messages("index.about")</a><br>
<a href="https://maven.gwm.dev/">@messagesProvider.messages("index.maven")</a><br>
<a href="https://gitea.gwm.dev/">@messagesProvider.messages("index.gitea")</a><br>
@if(admin) {
<a href="/admin">@messagesProvider.messages("index.admin")</a><br>
}
<script src="@routes.Assets.versioned("javascripts/main.js")" type="text/javascript"></script>
}

+ 7
- 0
app/views/profile.scala.html View File

@@ -0,0 +1,7 @@
@import entity.User

@(user: User)(implicit messagesProvider: MessagesProvider, optUsername: Option[String] = None)

@generic.main(messagesProvider.messages("profile.title")) {
<span>Hello in @user.name's profile!</span>
}

+ 16
- 0
app/views/registration.scala.html View File

@@ -0,0 +1,16 @@
@import helper._
@import forms.RegisterForm

@(userForm: Form[RegisterForm.Data])(implicit request: RequestHeader, messagesProvider: MessagesProvider, optUsername: Option[String] = None)


@generic.main(messagesProvider.messages("register.title")) {
<h1><b>Sign-up</b></h1>
@helper.form(action = routes.AuthController.register()) {
@CSRF.formField
@helper.inputText(userForm("name"))
@helper.inputText(userForm("email"))
@helper.inputPassword(userForm("password"))
<input type="submit">
}
}

+ 14
- 0
app/views/selfprofile.scala.html View File

@@ -0,0 +1,14 @@
@import entity.User

@import entity.Role
@(user: User, roles: Seq[Role])(implicit messagesProvider: MessagesProvider, optUsername: Option[String] = None)

@generic.main(messagesProvider.messages("selfprofile.title")) {
<span>Hello in your profile, @user.name</span>
@if(roles.nonEmpty) {
<span>Your roles: </span><br>
@for(role <- roles) {
<span>@role.name</span>
}
}
}

+ 26
- 0
build.sbt View File

@@ -0,0 +1,26 @@
name := "GWMDevelopmentsSite"
version := "1.0"

organization := "dev.gwm"
enablePlugins(PlayScala)

resolvers += "scalaz-bintray" at "https://dl.bintray.com/scalaz/releases"
resolvers += "Akka Snapshot Repository" at "https://repo.akka.io/snapshots/"
scalaVersion := "2.13.1"

libraryDependencies ++= Seq(
specs2 % Test,
guice,
"mysql" % "mysql-connector-java" % "8.0.18",
"com.typesafe.slick" %% "slick" % "3.3.2",
"com.typesafe.play" %% "play-slick" % "5.0.0",
"com.typesafe.play" %% "play-slick-evolutions" % "5.0.0",
"com.typesafe.play" %% "play-mailer" % "7.0.1",
"com.typesafe.play" %% "play-mailer-guice" % "7.0.1",
"at.favre.lib" % "bcrypt" % "0.9.0")

unmanagedResourceDirectories in Test += baseDirectory ( _ /"target/web/public/test" ).value

+ 18
- 0
conf/messages View File

@@ -0,0 +1,18 @@
index.title=GWM dev
index.files=Files
index.registration=Sign-up
index.login=Sign-in
index.logout=Logout
index.admin=Admin page
index.about=About me
index.maven=Maven repository
index.gitea=Gitea instance

fileslist.title=GWMDevelopments files

gwmcrates.title=GWMCrates

register.title=GWMDevelopments sign-up
login.title=GWMDevelopments sign-in

admin.title=Administrator''s page

+ 18
- 0
conf/messages.ru View File

@@ -0,0 +1,18 @@
index.title=GWM dev
index.files=Файлы
index.registration=Зарегистрироваться
index.login=Войти
index.logout=Выйти
index.admin=Страничка админа
index.about=Обо мне
index.maven=Maven репозиторий
index.gitea=Gitea хостинг

fileslist.title=GWMDevelopments файлы

gwmcrates.title=GWMCrates вики

register.title=GWMDevelopments регистрация
login.title=GWMDevelopments вход

admin.title=Страничка администратора

+ 35
- 0
conf/routes View File

@@ -0,0 +1,35 @@
# Routes
# This file defines all application routes (Higher priority routes first)
# https://www.playframework.com/documentation/latest/ScalaRouting
# ~~~~

# An example controller showing a sample home page
GET / controllers.HomeController.home
GET /home controllers.HomeController.home
GET /about controllers.HomeController.about

GET /files controllers.FilesController.files(name = "")
GET /files/*name controllers.FilesController.files(name: String)

POST /upload controllers.FilesController.upload

GET /registration controllers.AuthController.registration
POST /register controllers.AuthController.register
GET /authorization controllers.AuthController.authorization
POST /login controllers.AuthController.login
GET /confirm/:code controllers.AuthController.confirmRegistration(code: String)
GET /logout controllers.AuthController.logout

GET /profile controllers.UserController.selfProfile
GET /profile/id/:id controllers.UserController.userProfileById(id: Long)
GET /profile/name/:name controllers.UserController.userProfileByName(name: String)

GET /api/rest/users controllers.rest.UsersController.users
GET /api/rest/users/$id<\d+> controllers.rest.UsersController.user(id: Long, withRoles: Boolean ?= false)
GET /api/rest/roles controllers.rest.RolesController.roles
GET /api/rest/roles/$id<\d+> controllers.rest.RolesController.role(id: Long)

GET /admin controllers.AdminController.admin

# Map static resources from the /public folder to the /assets URL path
GET /assets/*file controllers.Assets.versioned(path="/public", file: Asset)

+ 1
- 0
project/build.properties View File

@@ -0,0 +1 @@
sbt.version=1.3.5

+ 5
- 0
project/plugins.sbt View File

@@ -0,0 +1,5 @@
logLevel := Level.Warn

resolvers += "Typesafe repository" at "https://repo.typesafe.com/typesafe/releases/"

addSbtPlugin("com.typesafe.play" % "sbt-plugin" % "2.8.0")

BIN
public/images/favicon.png View File

Before After
Width: 64  |  Height: 64  |  Size: 2.6KB

BIN
public/images/logo.png View File

Before After
Width: 64  |  Height: 64  |  Size: 2.6KB

+ 0
- 0
public/javascripts/main.js View File


+ 3
- 0
public/stylesheets/filenotfound.css View File

@@ -0,0 +1,3 @@
body {
background-color: coral;
}

+ 6
- 0
public/stylesheets/fileslist.css View File

@@ -0,0 +1,6 @@
body {
background-color: coral;
}
caption {
text-align: left
}

+ 44
- 0
public/stylesheets/generic/main.css View File

@@ -0,0 +1,44 @@
.header {
background-color: #f00000;
min-height: 64px;
padding: 16px;
display: flex;
}
.footer {
background-color: #cc0000;
min-height: 64px;
padding: 16px;
}
.content {
background-color: #FFB1A0;
}
.logo-img {
margin-right: 10px;
}
.spasite-menya-zastivili-delat-frontend {
display: flex;
align-items: flex-start;
}
.user-info {
align-items: flex-start;
margin-left: auto;
}
* {
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
}
.wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.content {
flex: 1 0 auto;
}
.footer {
flex: 0 0 auto;
}

+ 6
- 0
public/stylesheets/home.css View File

@@ -0,0 +1,6 @@
body {
background-color: coral;
}
body {
margin: 0;
}

+ 10
- 0
test/Test1.scala View File

@@ -0,0 +1,10 @@
import org.specs2.mutable.Specification

class Test1 extends Specification {

"Something" should {
"Do something" in {
failure("I've failed you, I'm so sorry :-(")
}
}
}

Loading…
Cancel
Save