Big refactoring #77

Merged
flecomte merged 166 commits from refactoring-component-and-immutable into master 2021-03-24 19:06:07 +01:00
27 changed files with 492 additions and 420 deletions
Showing only changes of commit 667339979b - Show all commits

View File

@@ -7,28 +7,15 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy
import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.databind.SerializationFeature
import com.fasterxml.jackson.datatype.joda.JodaModule import com.fasterxml.jackson.datatype.joda.JodaModule
import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException
import component.auth.jwt.jwtInstallation
import fr.dcproject.application.Env.PROD import fr.dcproject.application.Env.PROD
import fr.dcproject.component.article.routes.findArticleVersions import fr.dcproject.component.article.routes.installArticleRoutes
import fr.dcproject.component.article.routes.findArticles
import fr.dcproject.component.article.routes.getOneArticle
import fr.dcproject.component.article.routes.upsertArticle
import fr.dcproject.component.auth.ForbiddenException import fr.dcproject.component.auth.ForbiddenException
import fr.dcproject.component.auth.routes.authLogin import fr.dcproject.component.auth.jwt.jwtInstallation
import fr.dcproject.component.auth.routes.authPasswordless import fr.dcproject.component.auth.routes.installAuthRoutes
import fr.dcproject.component.auth.routes.authRegister
import fr.dcproject.component.auth.user import fr.dcproject.component.auth.user
import fr.dcproject.component.citizen.routes.changeMyPassword import fr.dcproject.component.citizen.routes.installCitizenRoutes
import fr.dcproject.component.citizen.routes.findCitizen import fr.dcproject.component.comment.article.routes.installCommentArticleRoutes
import fr.dcproject.component.citizen.routes.getCurrentCitizen import fr.dcproject.component.comment.generic.routes.installCommentRoutes
import fr.dcproject.component.citizen.routes.getOneCitizen
import fr.dcproject.component.comment.article.routes.createCommentArticle
import fr.dcproject.component.comment.article.routes.getArticleComments
import fr.dcproject.component.comment.article.routes.getCitizenArticleComments
import fr.dcproject.component.comment.generic.routes.createCommentChildren
import fr.dcproject.component.comment.generic.routes.editComment
import fr.dcproject.component.comment.generic.routes.getChildrenComments
import fr.dcproject.component.comment.generic.routes.getOneComment
import fr.dcproject.component.follow.routes.article.FollowArticle.followArticle import fr.dcproject.component.follow.routes.article.FollowArticle.followArticle
import fr.dcproject.component.follow.routes.article.GetFollowArticle.getFollowArticle import fr.dcproject.component.follow.routes.article.GetFollowArticle.getFollowArticle
import fr.dcproject.component.follow.routes.article.GetMyFollowsArticle.getMyFollowsArticle import fr.dcproject.component.follow.routes.article.GetMyFollowsArticle.getMyFollowsArticle
@@ -86,14 +73,14 @@ import io.ktor.routing.Routing
import io.ktor.server.jetty.EngineMain import io.ktor.server.jetty.EngineMain
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
import io.ktor.websocket.WebSockets import io.ktor.websocket.WebSockets
import java.time.Duration
import java.util.concurrent.CompletionException
import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.ExperimentalCoroutinesApi
import org.eclipse.jetty.util.log.Slf4jLog import org.eclipse.jetty.util.log.Slf4jLog
import org.koin.core.qualifier.named import org.koin.core.qualifier.named
import org.koin.ktor.ext.Koin import org.koin.ktor.ext.Koin
import org.koin.ktor.ext.get import org.koin.ktor.ext.get
import org.slf4j.event.Level import org.slf4j.event.Level
import java.time.Duration
import java.util.concurrent.CompletionException
fun main(args: Array<String>): Unit = EngineMain.main(args) fun main(args: Array<String>): Unit = EngineMain.main(args)
@@ -158,30 +145,13 @@ fun Application.module(env: Env = PROD) {
install(Routing.Feature) { install(Routing.Feature) {
// trace { application.log.trace(it.buildText()) } // trace { application.log.trace(it.buildText()) }
installArticleRoutes()
installAuthRoutes()
installCitizenRoutes()
installCommentArticleRoutes()
installCommentRoutes()
authenticate(optional = true) { authenticate(optional = true) {
/* Article */
findArticles(get(), get())
getOneArticle(get(), get())
upsertArticle(get(), get(), get())
findArticleVersions(get(), get())
/* Citizen */
findCitizen(get(), get())
getOneCitizen(get())
getCurrentCitizen(get())
changeMyPassword(get(), get())
/* Comment */
editComment(get(), get())
getOneComment(get(), get())
createCommentChildren(get(), get())
getChildrenComments(get(), get())
/* Comment Article */
getArticleComments(get(), get())
createCommentArticle(get(), get())
getCitizenArticleComments(get(), get())
/* Auth */
authLogin(get())
authRegister(get())
authPasswordless(get())
/* Workgroup */ /* Workgroup */
getWorkgroups(get(), get()) getWorkgroups(get(), get())
getWorkgroup(get(), get()) getWorkgroup(get(), get())

View File

@@ -14,28 +14,28 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/articles/{article}/versions") object FindArticleVersions {
class ArticleVersionsRequest( @Location("/articles/{article}/versions")
val article: ArticleForView, class ArticleVersionsRequest(
page: Int = 1, val article: ArticleForView,
limit: Int = 50, page: Int = 1,
val sort: String? = null, limit: Int = 50,
val direction: RepositoryI.Direction? = null, val sort: String? = null,
val search: String? = null val direction: RepositoryI.Direction? = null,
) { val search: String? = null
val page: Int = if (page < 1) 1 else page ) {
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit val page: Int = if (page < 1) 1 else page
} val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
@KtorExperimentalLocationsAPI private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) =
private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) = findVersionsByVersionId(request.page, request.limit, request.article.versionId)
findVersionsByVersionId(request.page, request.limit, request.article.versionId)
@KtorExperimentalLocationsAPI fun Route.findArticleVersions(repo: ArticleRepository, voter: ArticleVoter) {
fun Route.findArticleVersions(repo: ArticleRepository, voter: ArticleVoter) { get<ArticleVersionsRequest> {
get<ArticleVersionsRequest> { repo.findVersions(it)
repo.findVersions(it) .apply { voter.assert { canView(it.article, citizenOrNull) } }
.apply { voter.assert { canView(it.article, citizenOrNull) } } .let { call.respond(it) }
.let { call.respond(it) } }
} }
} }

View File

@@ -8,40 +8,44 @@ import fr.dcproject.voter.assert
import fr.postgresjson.connexion.Paginated import fr.postgresjson.connexion.Paginated
import fr.postgresjson.repository.RepositoryI import fr.postgresjson.repository.RepositoryI
import io.ktor.application.call import io.ktor.application.call
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location import io.ktor.locations.Location
import io.ktor.locations.get import io.ktor.locations.get
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@Location("/articles") @KtorExperimentalLocationsAPI
class ArticlesRequest( object FindArticles {
page: Int = 1, @Location("/articles")
limit: Int = 50, class ArticlesRequest(
val sort: String? = null, page: Int = 1,
val direction: RepositoryI.Direction? = null, limit: Int = 50,
val search: String? = null, val sort: String? = null,
val createdBy: String? = null, val direction: RepositoryI.Direction? = null,
val workgroup: String? = null val search: String? = null,
) { val createdBy: String? = null,
val page: Int = if (page < 1) 1 else page val workgroup: String? = null
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit ) {
} val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<ArticleForListing> { private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<ArticleForListing> {
return find( return find(
request.page, request.page,
request.limit, request.limit,
request.sort, request.sort,
request.direction, request.direction,
request.search, request.search,
ArticleRepository.Filter(createdById = request.createdBy, workgroupId = request.workgroup) ArticleRepository.Filter(createdById = request.createdBy, workgroupId = request.workgroup)
) )
} }
fun Route.findArticles(repo: ArticleRepository, voter: ArticleVoter) { fun Route.findArticles(repo: ArticleRepository, voter: ArticleVoter) {
get<ArticlesRequest> { get<ArticlesRequest> {
repo.findArticles(it) repo.findArticles(it)
.apply { voter.assert { canView(result, citizenOrNull) } } .apply { voter.assert { canView(result, citizenOrNull) } }
.let { call.respond(it) } .let { call.respond(it) }
}
} }
} }

View File

@@ -4,7 +4,7 @@ import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRepository import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleViewManager import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.ArticleVoter import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.component.article.routes.ArticleRequest.Output import fr.dcproject.component.article.routes.GetOneArticle.ArticleRequest.Output
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.dto.CreatedAt import fr.dcproject.dto.CreatedAt
import fr.dcproject.dto.Opinionable import fr.dcproject.dto.Opinionable
@@ -19,56 +19,54 @@ import io.ktor.locations.Location
import io.ktor.locations.get import io.ktor.locations.get
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.koin.core.KoinComponent import org.koin.core.KoinComponent
import org.koin.core.inject import org.koin.core.inject
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/articles/{articleId}") object GetOneArticle {
class ArticleRequest(val articleId: UUID) : KoinComponent { @Location("/articles/{articleId}")
val repo: ArticleRepository by inject() class ArticleRequest(val articleId: UUID) : KoinComponent {
val repo: ArticleRepository by inject()
@KtorExperimentalAPI val article: ArticleForView = repo.findById(articleId) ?: throw NotFoundException("Article $articleId not found")
val article: ArticleForView = repo.findById(articleId) ?: throw NotFoundException("Article $articleId not found")
class Output( class Output(
article: ArticleForView, article: ArticleForView,
views: fr.dcproject.entity.ViewAggregation = fr.dcproject.entity.ViewAggregation() views: fr.dcproject.entity.ViewAggregation = fr.dcproject.entity.ViewAggregation()
) : CreatedAt by CreatedAt.Imp(article), ) : CreatedAt by CreatedAt.Imp(article),
Opinionable by Opinionable.Imp(article), Opinionable by Opinionable.Imp(article),
Votable by Votable.Imp(article), Votable by Votable.Imp(article),
Versionable by Versionable.Imp(article), Versionable by Versionable.Imp(article),
Viewable by Viewable.Imp(views) { Viewable by Viewable.Imp(views) {
val id = article.id val id = article.id
val title = article.title val title = article.title
val anonymous = article.anonymous val anonymous = article.anonymous
val content = article.content val content = article.content
val description = article.description val description = article.description
val tags = article.tags val tags = article.tags
val draft = article.draft val draft = article.draft
val lastVersion = article.lastVersion val lastVersion = article.lastVersion
val createdBy = article.createdBy val createdBy = article.createdBy
val workgroup = article.workgroup // TODO change to workgroup DTO val workgroup = article.workgroup // TODO change to workgroup DTO
}
}
@KtorExperimentalAPI
@KtorExperimentalLocationsAPI
fun Route.getOneArticle(viewManager: ArticleViewManager, voter: ArticleVoter) {
get<ArticleRequest> {
voter.assert { canView(it.article, citizenOrNull) }
Output(
it.article,
viewManager.getViewsCount(it.article)
).also { out ->
call.respond(out)
} }
}
launch { fun Route.getOneArticle(viewManager: ArticleViewManager, voter: ArticleVoter) {
viewManager.addView(call.request.local.remoteHost, it.article, citizenOrNull) get<ArticleRequest> {
voter.assert { canView(it.article, citizenOrNull) }
Output(
it.article,
viewManager.getViewsCount(it.article)
).also { out ->
call.respond(out)
}
launch {
viewManager.addView(call.request.local.remoteHost, it.article, citizenOrNull)
}
} }
} }
} }

View File

@@ -4,7 +4,7 @@ import fr.dcproject.component.article.ArticleForUpdate
import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRepository import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleVoter import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.component.article.routes.PostArticleRequest.Input import fr.dcproject.component.article.routes.UpsertArticle.UpsertArticleRequest.Input
import fr.dcproject.component.auth.citizen import fr.dcproject.component.auth.citizen
import fr.dcproject.component.auth.citizenOrNull import fr.dcproject.component.auth.citizenOrNull
import fr.dcproject.component.workgroup.WorkgroupRef import fr.dcproject.component.workgroup.WorkgroupRef
@@ -23,47 +23,44 @@ import io.ktor.routing.Route
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/articles") object UpsertArticle {
class PostArticleRequest { @Location("/articles")
class Input( class UpsertArticleRequest {
val id: UUID?, class Input(
val title: String, val id: UUID?,
val anonymous: Boolean = true, val title: String,
val content: String, val anonymous: Boolean = true,
val description: String, val content: String,
val tags: List<String> = emptyList(), val description: String,
val draft: Boolean = false, val tags: List<String> = emptyList(),
val versionId: UUID?, val draft: Boolean = false,
val workgroup: WorkgroupRef? = null val versionId: UUID?,
) val workgroup: WorkgroupRef? = null
}
@KtorExperimentalLocationsAPI
fun Route.upsertArticle(repo: ArticleRepository, workgroupRepository: WorkgroupRepository, voter: ArticleVoter) {
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receive<Input>().run {
ArticleForUpdate(
id = id ?: UUID.randomUUID(),
title = title,
anonymous = anonymous,
content = content,
description = description,
tags = tags,
draft = draft,
createdBy = citizen,
workgroup = if (workgroup != null) workgroupRepository.findById(workgroup.id) else null,
versionId = versionId
) )
} }
post<PostArticleRequest> { fun Route.upsertArticle(repo: ArticleRepository, workgroupRepository: WorkgroupRepository, voter: ArticleVoter) {
val article = call.convertRequestToEntity() suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receive<Input>().run {
ArticleForUpdate(
id = id ?: UUID.randomUUID(),
title = title,
anonymous = anonymous,
content = content,
description = description,
tags = tags,
draft = draft,
createdBy = citizen,
workgroup = if (workgroup != null) workgroupRepository.findById(workgroup.id) else null,
versionId = versionId
)
}
voter.assert { canUpsert(article, citizenOrNull) } post<UpsertArticleRequest> {
val article = call.convertRequestToEntity()
val newArticle: ArticleForView = repo.upsert(article) ?: error("Article not updated") voter.assert { canUpsert(article, citizenOrNull) }
val newArticle: ArticleForView = repo.upsert(article) ?: error("Article not updated")
call.respond(newArticle) call.respond(newArticle)
raiseEvent(ArticleUpdate.event, ArticleUpdate(newArticle))
raiseEvent(ArticleUpdate.event, ArticleUpdate(newArticle)) }
} }
} }

View File

@@ -0,0 +1,21 @@
package fr.dcproject.component.article.routes
import fr.dcproject.component.article.routes.FindArticleVersions.findArticleVersions
import fr.dcproject.component.article.routes.FindArticles.findArticles
import fr.dcproject.component.article.routes.GetOneArticle.getOneArticle
import fr.dcproject.component.article.routes.UpsertArticle.upsertArticle
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installArticleRoutes() {
authenticate(optional = true) {
findArticles(get(), get())
findArticleVersions(get(), get())
getOneArticle(get(), get())
upsertArticle(get(), get(), get())
}
}

View File

@@ -6,7 +6,7 @@ import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.component.citizen.CitizenRepository import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.component.citizen.CitizenWithEmail import fr.dcproject.component.citizen.CitizenWithEmail
import fr.dcproject.component.citizen.CitizenWithUserI import fr.dcproject.component.citizen.CitizenWithUserI
import fr.dcproject.makeToken import fr.dcproject.component.auth.jwt.makeToken
import fr.dcproject.messages.Mailer import fr.dcproject.messages.Mailer
import io.ktor.http.URLBuilder import io.ktor.http.URLBuilder

View File

@@ -1,4 +1,4 @@
package fr.dcproject package fr.dcproject.component.auth.jwt
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import fr.dcproject.component.auth.UserI import fr.dcproject.component.auth.UserI

View File

@@ -1,8 +1,7 @@
package component.auth.jwt package fr.dcproject.component.auth.jwt
import fr.dcproject.component.auth.User import fr.dcproject.component.auth.User
import fr.dcproject.component.auth.UserRepository import fr.dcproject.component.auth.UserRepository
import fr.dcproject.component.auth.jwt.JwtConfig
import io.ktor.application.ApplicationCall import io.ktor.application.ApplicationCall
import io.ktor.auth.Authentication import io.ktor.auth.Authentication
import io.ktor.auth.jwt.jwt import io.ktor.auth.jwt.jwt

View File

@@ -2,7 +2,7 @@ package fr.dcproject.component.auth.routes
import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.fasterxml.jackson.databind.exc.MismatchedInputException
import fr.dcproject.component.auth.UserRepository import fr.dcproject.component.auth.UserRepository
import fr.dcproject.makeToken import fr.dcproject.component.auth.jwt.makeToken
import io.ktor.application.call import io.ktor.application.call
import io.ktor.auth.UserPasswordCredential import io.ktor.auth.UserPasswordCredential
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -13,23 +13,22 @@ import io.ktor.request.receive
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/login") object Login {
class LoginRequest @Location("/login")
class LoginRequest
@KtorExperimentalLocationsAPI fun Route.authLogin(userRepo: UserRepository) {
@KtorExperimentalAPI post<LoginRequest> {
fun Route.authLogin(userRepo: UserRepository) { try {
post<LoginRequest> { val credentials = call.receive<UserPasswordCredential>()
try { userRepo.findByCredentials(credentials)?.let { user ->
val credentials = call.receive<UserPasswordCredential>() call.respondText(user.makeToken())
userRepo.findByCredentials(credentials)?.let { user -> } ?: call.respond(HttpStatusCode.BadRequest, "Username not exist or password is wrong")
call.respondText(user.makeToken()) } catch (e: MismatchedInputException) {
} ?: call.respond(HttpStatusCode.BadRequest, "Username not exist or password is wrong") call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request")
} catch (e: MismatchedInputException) { }
call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request")
} }
} }
} }

View File

@@ -3,11 +3,11 @@ package fr.dcproject.component.auth.routes
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.component.auth.User import fr.dcproject.component.auth.User
import fr.dcproject.component.auth.UserI import fr.dcproject.component.auth.UserI
import fr.dcproject.component.auth.routes.RegisterRequest.Input import fr.dcproject.component.auth.routes.Register.RegisterRequest.Input
import fr.dcproject.component.citizen.Citizen import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenI import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRepository import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.makeToken import fr.dcproject.component.auth.jwt.makeToken
import io.ktor.application.call import io.ktor.application.call
import io.ktor.features.BadRequestException import io.ktor.features.BadRequestException
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
@@ -18,55 +18,54 @@ import io.ktor.request.receive
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.response.respondText import io.ktor.response.respondText
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
import org.joda.time.DateTime import org.joda.time.DateTime
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/register") object Register {
class RegisterRequest { @Location("/register")
data class Input( class RegisterRequest {
val name: Name, data class Input(
val email: String, val name: Name,
val birthday: DateTime, val email: String,
val voteAnonymous: Boolean = true, val birthday: DateTime,
val followAnonymous: Boolean = true, val voteAnonymous: Boolean = true,
val user: User val followAnonymous: Boolean = true,
) { val user: User
data class Name( ) {
val firstName: String, data class Name(
val lastName: String, val firstName: String,
val civility: String? = null val lastName: String,
) val civility: String? = null
data class User( )
val username: String, data class User(
val plainPassword: String? = null val username: String,
) val plainPassword: String? = null
)
}
} }
}
@KtorExperimentalLocationsAPI fun Route.authRegister(citizenRepo: CitizenRepository) {
@KtorExperimentalAPI fun Input.toCitizen(): Citizen = Citizen(
fun Route.authRegister(citizenRepo: CitizenRepository) { name = CitizenI.Name(name.firstName, name.lastName, name.civility),
fun Input.toCitizen(): Citizen = Citizen( birthday = birthday,
name = CitizenI.Name(name.firstName, name.lastName, name.civility), email = email,
birthday = birthday, followAnonymous = followAnonymous,
email = email, voteAnonymous = voteAnonymous,
followAnonymous = followAnonymous, user = User(
voteAnonymous = voteAnonymous, username = user.username,
user = User( plainPassword = user.plainPassword,
username = user.username, roles = listOf(UserI.Roles.ROLE_USER)
plainPassword = user.plainPassword, )
roles = listOf(UserI.Roles.ROLE_USER)
) )
)
post<RegisterRequest> { post<RegisterRequest> {
try { try {
val citizen = call.receive<Input>().toCitizen() val citizen = call.receive<Input>().toCitizen()
val createdCitizen = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request") val createdCitizen = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request")
call.respondText(createdCitizen.makeToken()) call.respondText(createdCitizen.makeToken())
} catch (e: MissingKotlinParameterException) { } catch (e: MissingKotlinParameterException) {
call.respond(HttpStatusCode.BadRequest) call.respond(HttpStatusCode.BadRequest)
}
} }
} }
} }

View File

@@ -1,7 +1,7 @@
package fr.dcproject.component.auth.routes package fr.dcproject.component.auth.routes
import fr.dcproject.component.auth.PasswordlessAuth import fr.dcproject.component.auth.PasswordlessAuth
import fr.dcproject.component.auth.routes.PasswordlessRequest.Input import fr.dcproject.component.auth.routes.Sso.PasswordlessRequest.Input
import io.ktor.application.call import io.ktor.application.call
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.KtorExperimentalLocationsAPI
@@ -10,28 +10,27 @@ import io.ktor.locations.post
import io.ktor.request.receive import io.ktor.request.receive
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/auth/passwordless") object Sso {
class PasswordlessRequest { @Location("/auth/passwordless")
data class Input(val email: String, val url: String) class PasswordlessRequest {
} data class Input(val email: String, val url: String)
}
/** /**
* Send an email to the citizen with a link to automatically connect * Send an email to the citizen with a link to automatically connect
*/ */
@KtorExperimentalLocationsAPI fun Route.authPasswordless(passwordlessAuth: PasswordlessAuth) {
@KtorExperimentalAPI post<PasswordlessRequest> {
fun Route.authPasswordless(passwordlessAuth: PasswordlessAuth) { call.receive<Input>().run {
post<PasswordlessRequest> { try {
call.receive<Input>().run { passwordlessAuth.sendEmail(email, url)
try { } catch (e: PasswordlessAuth.EmailNotFound) {
passwordlessAuth.sendEmail(email, url) call.respond(HttpStatusCode.NotFound)
} catch (e: PasswordlessAuth.EmailNotFound) { }
call.respond(HttpStatusCode.NotFound) call.respond(HttpStatusCode.NoContent)
} }
call.respond(HttpStatusCode.NoContent)
} }
} }
} }

View File

@@ -0,0 +1,19 @@
package fr.dcproject.component.auth.routes
import fr.dcproject.component.auth.routes.Login.authLogin
import fr.dcproject.component.auth.routes.Register.authRegister
import fr.dcproject.component.auth.routes.Sso.authPasswordless
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installAuthRoutes() {
authenticate(optional = true) {
authLogin(get())
authRegister(get())
authPasswordless(get())
}
}

View File

@@ -18,30 +18,30 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object ChangeMyPassword {
@Location("/citizens/{citizen}/password/change")
class ChangePasswordCitizenRequest(val citizen: Citizen) {
data class Input(val oldPassword: String, val newPassword: String)
}
@Location("/citizens/{citizen}/password/change") fun Route.changeMyPassword(voter: CitizenVoter, userRepository: UserRepository) {
class ChangePasswordCitizenRequest(val citizen: Citizen) { put<ChangePasswordCitizenRequest> {
data class Input(val oldPassword: String, val newPassword: String) voter.assert { canChangePassword(it.citizen, citizenOrNull) }
} try {
val content = call.receive<ChangePasswordCitizenRequest.Input>()
val currentUser = userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword))
val user = it.citizen.user
if (currentUser == null || currentUser.id != user.id) {
call.respond(HttpStatusCode.BadRequest, "Bad password")
} else {
user.plainPassword = content.newPassword
userRepository.changePassword(user)
@KtorExperimentalLocationsAPI call.respond(HttpStatusCode.Created)
fun Route.changeMyPassword(voter: CitizenVoter, userRepository: UserRepository) { }
put<ChangePasswordCitizenRequest> { } catch (e: MissingKotlinParameterException) {
voter.assert { canChangePassword(it.citizen, citizenOrNull) } call.respond(HttpStatusCode.BadRequest, "Request format is not correct")
try {
val content = call.receive<ChangePasswordCitizenRequest.Input>()
val currentUser = userRepository.findByCredentials(UserPasswordCredential(citizen.user.username, content.oldPassword))
val user = it.citizen.user
if (currentUser == null || currentUser.id != user.id) {
call.respond(HttpStatusCode.BadRequest, "Bad password")
} else {
user.plainPassword = content.newPassword
userRepository.changePassword(user)
call.respond(HttpStatusCode.Created)
} }
} catch (e: MissingKotlinParameterException) {
call.respond(HttpStatusCode.BadRequest, "Request format is not correct")
} }
} }
} }

View File

@@ -13,23 +13,24 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/citizens") object FindCitizens {
class CitizensRequest( @Location("/citizens")
page: Int = 1, class CitizensRequest(
limit: Int = 50, page: Int = 1,
val sort: String? = null, limit: Int = 50,
val direction: RepositoryI.Direction? = null, val sort: String? = null,
val search: String? = null val direction: RepositoryI.Direction? = null,
) { val search: String? = null
val page: Int = if (page < 1) 1 else page ) {
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit val page: Int = if (page < 1) 1 else page
} val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
@KtorExperimentalLocationsAPI fun Route.findCitizen(voter: CitizenVoter, repo: CitizenRepository) {
fun Route.findCitizen(voter: CitizenVoter, repo: CitizenRepository) { get<CitizensRequest> {
get<CitizensRequest> { val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) voter.assert { canView(citizens.result, citizenOrNull) }
voter.assert { canView(citizens.result, citizenOrNull) } call.respond(citizens)
call.respond(citizens) }
} }
} }

View File

@@ -13,18 +13,19 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/citizens/current") object GetCurrentCitizen {
class CurrentCitizenRequest @Location("/citizens/current")
class CurrentCitizenRequest
@KtorExperimentalLocationsAPI fun Route.getCurrentCitizen(voter: CitizenVoter) {
fun Route.getCurrentCitizen(voter: CitizenVoter) { get<CurrentCitizenRequest> {
get<CurrentCitizenRequest> { val currentUser = citizenOrNull
val currentUser = citizenOrNull if (currentUser === null) {
if (currentUser === null) { call.respond(HttpStatusCode.Unauthorized)
call.respond(HttpStatusCode.Unauthorized) } else {
} else { voter.assert { canView(currentUser, citizenOrNull) }
voter.assert { canView(currentUser, citizenOrNull) } call.respond(citizen)
call.respond(citizen) }
} }
} }
} }

View File

@@ -12,14 +12,15 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/citizens/{citizen}") object GetOneCitizen {
class CitizenRequest(val citizen: Citizen) @Location("/citizens/{citizen}")
class CitizenRequest(val citizen: Citizen)
@KtorExperimentalLocationsAPI fun Route.getOneCitizen(voter: CitizenVoter) {
fun Route.getOneCitizen(voter: CitizenVoter) { get<CitizenRequest> {
get<CitizenRequest> { voter.assert { canView(it.citizen, citizenOrNull) }
voter.assert { canView(it.citizen, citizenOrNull) }
call.respond(it.citizen) call.respond(it.citizen)
}
} }
} }

View File

@@ -0,0 +1,24 @@
package fr.dcproject.component.citizen.routes
import fr.dcproject.component.auth.routes.Login.authLogin
import fr.dcproject.component.auth.routes.Register.authRegister
import fr.dcproject.component.auth.routes.Sso.authPasswordless
import fr.dcproject.component.citizen.routes.ChangeMyPassword.changeMyPassword
import fr.dcproject.component.citizen.routes.FindCitizens.findCitizen
import fr.dcproject.component.citizen.routes.GetCurrentCitizen.getCurrentCitizen
import fr.dcproject.component.citizen.routes.GetOneCitizen.getOneCitizen
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import io.ktor.util.KtorExperimentalAPI
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installCitizenRoutes() {
authenticate(optional = true) {
findCitizen(get(), get())
getOneCitizen(get())
getCurrentCitizen(get())
changeMyPassword(get(), get())
}
}

View File

@@ -18,30 +18,31 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/articles/{article}/comments") object CreateCommentArticle {
class PostArticleCommentRequest( @Location("/articles/{article}/comments")
val article: ArticleForView class PostArticleCommentRequest(
) { val article: ArticleForView
class Comment( ) {
val content: String class Comment(
) val content: String
suspend fun getComment(call: ApplicationCall) = call.receive<Comment>().run {
CommentForUpdate(
target = article,
createdBy = call.citizen,
content = content
) )
}
}
@KtorExperimentalLocationsAPI suspend fun getComment(call: ApplicationCall) = call.receive<Comment>().run {
fun Route.createCommentArticle(repo: CommentArticleRepository, voter: CommentVoter) { CommentForUpdate(
post<PostArticleCommentRequest> { target = article,
it.getComment(call).let { comment -> createdBy = call.citizen,
voter.assert { canCreate(comment, citizenOrNull) } content = content
repo.comment(comment) )
call.respond(HttpStatusCode.Created, comment) }
}
fun Route.createCommentArticle(repo: CommentArticleRepository, voter: CommentVoter) {
post<PostArticleCommentRequest> {
it.getComment(call).let { comment ->
voter.assert { canCreate(comment, citizenOrNull) }
repo.comment(comment)
call.respond(HttpStatusCode.Created, comment)
}
} }
} }
} }

View File

@@ -14,26 +14,27 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/articles/{article}/comments") object GetArticleComments {
class ArticleCommentsRequest( @Location("/articles/{article}/comments")
val article: ArticleRef, class ArticleCommentsRequest(
page: Int = 1, val article: ArticleRef,
limit: Int = 50, page: Int = 1,
val search: String? = null, limit: Int = 50,
sort: String = CommentArticleRepository.Sort.CREATED_AT.sql val search: String? = null,
) { sort: String = CommentArticleRepository.Sort.CREATED_AT.sql
val page: Int = if (page < 1) 1 else page ) {
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit val page: Int = if (page < 1) 1 else page
val sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.fromString(sort) ?: CommentArticleRepository.Sort.CREATED_AT val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
} val sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.fromString(sort) ?: CommentArticleRepository.Sort.CREATED_AT
}
@KtorExperimentalLocationsAPI fun Route.getArticleComments(repo: CommentArticleRepository, voter: CommentVoter) {
fun Route.getArticleComments(repo: CommentArticleRepository, voter: CommentVoter) { get<ArticleCommentsRequest> {
get<ArticleCommentsRequest> { val comment = repo.findByTarget(it.article, it.page, it.limit, it.sort)
val comment = repo.findByTarget(it.article, it.page, it.limit, it.sort) if (comment.result.isNotEmpty()) {
if (comment.result.isNotEmpty()) { voter.assert { canView(comment.result, citizenOrNull) }
voter.assert { canView(comment.result, citizenOrNull) } }
call.respond(HttpStatusCode.OK, comment)
} }
call.respond(HttpStatusCode.OK, comment)
} }
} }

View File

@@ -13,15 +13,16 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/citizens/{citizen}/comments/articles") object GetCitizenArticleComments {
class CitizenCommentArticleRequest(val citizen: Citizen) @Location("/citizens/{citizen}/comments/articles")
class CitizenCommentArticleRequest(val citizen: Citizen)
@KtorExperimentalLocationsAPI fun Route.getCitizenArticleComments(repo: CommentArticleRepository, voter: CommentVoter) {
fun Route.getCitizenArticleComments(repo: CommentArticleRepository, voter: CommentVoter) { get<CitizenCommentArticleRequest> {
get<CitizenCommentArticleRequest> { repo.findByCitizen(it.citizen).let { comments ->
repo.findByCitizen(it.citizen).let { comments -> voter.assert { canView(comments.result, citizenOrNull) }
voter.assert { canView(comments.result, citizenOrNull) } call.respond(comments)
call.respond(comments) }
} }
} }
} }

View File

@@ -0,0 +1,18 @@
package fr.dcproject.component.comment.article.routes
import fr.dcproject.component.comment.article.routes.CreateCommentArticle.createCommentArticle
import fr.dcproject.component.comment.article.routes.GetArticleComments.getArticleComments
import fr.dcproject.component.comment.article.routes.GetCitizenArticleComments.getCitizenArticleComments
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installCommentArticleRoutes() {
authenticate(optional = true) {
getArticleComments(get(), get())
createCommentArticle(get(), get())
getCitizenArticleComments(get(), get())
}
}

View File

@@ -19,25 +19,25 @@ import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/comments/{comment}/children") object CreateCommentChildren {
class CreateCommentChildrenRequest(val comment: CommentRef) { @Location("/comments/{comment}/children")
class Input(val content: String) class CreateCommentChildrenRequest(val comment: CommentRef) {
} class Input(val content: String)
}
@KtorExperimentalAPI fun Route.createCommentChildren(repo: CommentRepository, voter: CommentVoter) {
@KtorExperimentalLocationsAPI post<CreateCommentChildrenRequest> {
fun Route.createCommentChildren(repo: CommentRepository, voter: CommentVoter) { val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
post<CreateCommentChildrenRequest> { val newComment = CommentForUpdate(
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") content = call.receive<CreateCommentChildrenRequest.Input>().content,
val newComment = CommentForUpdate( createdBy = citizen,
content = call.receive<CreateCommentChildrenRequest.Input>().content, parent = parent
createdBy = citizen, )
parent = parent
)
voter.assert { canCreate(newComment, citizenOrNull) } voter.assert { canCreate(newComment, citizenOrNull) }
repo.comment(newComment) repo.comment(newComment)
call.respond(HttpStatusCode.Created, newComment) call.respond(HttpStatusCode.Created, newComment)
}
} }
} }

View File

@@ -14,22 +14,21 @@ import io.ktor.locations.put
import io.ktor.request.receiveText import io.ktor.request.receiveText
import io.ktor.response.respond import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/comments/{comment}") object EditComment {
class EditCommentRequest(val comment: CommentRef) @Location("/comments/{comment}")
class EditCommentRequest(val comment: CommentRef)
@KtorExperimentalAPI fun Route.editComment(repo: CommentRepository, voter: CommentVoter) {
@KtorExperimentalLocationsAPI put<EditCommentRequest> {
fun Route.editComment(repo: CommentRepository, voter: CommentVoter) { val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
put<EditCommentRequest> { voter.assert { canUpdate(comment, citizenOrNull) }
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
voter.assert { canUpdate(comment, citizenOrNull) }
comment.content = call.receiveText() comment.content = call.receiveText()
repo.edit(comment) repo.edit(comment)
call.respond(HttpStatusCode.OK, comment) call.respond(HttpStatusCode.OK, comment)
}
} }
} }

View File

@@ -15,30 +15,30 @@ import io.ktor.util.KtorExperimentalAPI
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/comments/{comment}/children") object GetCommentChildren {
class CommentChildrenRequest( @Location("/comments/{comment}/children")
val comment: UUID, class CommentChildrenRequest(
page: Int = 1, val comment: UUID,
limit: Int = 50, page: Int = 1,
val search: String? = null limit: Int = 50,
) { val search: String? = null
val page: Int = if (page < 1) 1 else page ) {
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit val page: Int = if (page < 1) 1 else page
} val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
}
@KtorExperimentalAPI fun Route.getChildrenComments(repo: CommentRepository, voter: CommentVoter) {
@KtorExperimentalLocationsAPI get<CommentChildrenRequest> {
fun Route.getChildrenComments(repo: CommentRepository, voter: CommentVoter) { val comments =
get<CommentChildrenRequest> { repo.findByParent(
val comments = it.comment,
repo.findByParent( it.page,
it.comment, it.limit
it.page, )
it.limit
)
voter.assert { canView(comments.result, citizenOrNull) } voter.assert { canView(comments.result, citizenOrNull) }
call.respond(HttpStatusCode.OK, comments) call.respond(HttpStatusCode.OK, comments)
}
} }
} }

View File

@@ -16,16 +16,16 @@ import io.ktor.routing.Route
import io.ktor.util.KtorExperimentalAPI import io.ktor.util.KtorExperimentalAPI
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/comments/{comment}") object GetOneComment {
class CommentRequest(val comment: CommentRef) @Location("/comments/{comment}")
class CommentRequest(val comment: CommentRef)
@KtorExperimentalAPI fun Route.getOneComment(repo: CommentRepository, voter: CommentVoter) {
@KtorExperimentalLocationsAPI get<CommentRequest> {
fun Route.getOneComment(repo: CommentRepository, voter: CommentVoter) { val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
get<CommentRequest> { voter.assert { canView(comment, citizenOrNull) }
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
voter.assert { canView(comment, citizenOrNull) }
call.respond(HttpStatusCode.OK, comment) call.respond(HttpStatusCode.OK, comment)
}
} }
} }

View File

@@ -0,0 +1,20 @@
package fr.dcproject.component.comment.generic.routes
import fr.dcproject.component.comment.generic.routes.CreateCommentChildren.createCommentChildren
import fr.dcproject.component.comment.generic.routes.EditComment.editComment
import fr.dcproject.component.comment.generic.routes.GetCommentChildren.getChildrenComments
import fr.dcproject.component.comment.generic.routes.GetOneComment.getOneComment
import io.ktor.auth.authenticate
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.routing.Routing
import org.koin.ktor.ext.get
@KtorExperimentalLocationsAPI
fun Routing.installCommentRoutes() {
authenticate(optional = true) {
editComment(get(), get())
getOneComment(get(), get())
createCommentChildren(get(), get())
getChildrenComments(get(), get())
}
}