move routes installation into component

This commit is contained in:
2021-01-21 21:55:24 +01:00
parent 3ba4a195ba
commit 667339979b
27 changed files with 492 additions and 420 deletions

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")
class ArticleVersionsRequest(
val article: ArticleForView, val article: ArticleForView,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val sort: String? = null, val sort: String? = null,
val direction: RepositoryI.Direction? = null, val direction: RepositoryI.Direction? = null,
val search: String? = null val search: String? = null
) { ) {
val page: Int = if (page < 1) 1 else page val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit 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,13 +8,16 @@ 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 {
@Location("/articles")
class ArticlesRequest(
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val sort: String? = null, val sort: String? = null,
@@ -22,12 +25,12 @@ class ArticlesRequest(
val search: String? = null, val search: String? = null,
val createdBy: String? = null, val createdBy: String? = null,
val workgroup: String? = null val workgroup: String? = null
) { ) {
val page: Int = if (page < 1) 1 else page val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit 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,
@@ -36,12 +39,13 @@ private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated<
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,18 +19,17 @@ 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}")
class ArticleRequest(val articleId: UUID) : KoinComponent {
val repo: ArticleRepository by inject() 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(
@@ -52,11 +51,9 @@ class ArticleRequest(val articleId: UUID) : KoinComponent {
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 fun Route.getOneArticle(viewManager: ArticleViewManager, voter: ArticleVoter) {
@KtorExperimentalLocationsAPI
fun Route.getOneArticle(viewManager: ArticleViewManager, voter: ArticleVoter) {
get<ArticleRequest> { get<ArticleRequest> {
voter.assert { canView(it.article, citizenOrNull) } voter.assert { canView(it.article, citizenOrNull) }
@@ -71,4 +68,5 @@ fun Route.getOneArticle(viewManager: ArticleViewManager, voter: ArticleVoter) {
viewManager.addView(call.request.local.remoteHost, it.article, citizenOrNull) 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,8 +23,9 @@ import io.ktor.routing.Route
import java.util.UUID import java.util.UUID
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
@Location("/articles") object UpsertArticle {
class PostArticleRequest { @Location("/articles")
class UpsertArticleRequest {
class Input( class Input(
val id: UUID?, val id: UUID?,
val title: String, val title: String,
@@ -36,10 +37,9 @@ class PostArticleRequest {
val versionId: UUID?, val versionId: UUID?,
val workgroup: WorkgroupRef? = null val workgroup: WorkgroupRef? = null
) )
} }
@KtorExperimentalLocationsAPI fun Route.upsertArticle(repo: ArticleRepository, workgroupRepository: WorkgroupRepository, voter: ArticleVoter) {
fun Route.upsertArticle(repo: ArticleRepository, workgroupRepository: WorkgroupRepository, voter: ArticleVoter) {
suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receive<Input>().run { suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receive<Input>().run {
ArticleForUpdate( ArticleForUpdate(
id = id ?: UUID.randomUUID(), id = id ?: UUID.randomUUID(),
@@ -55,15 +55,12 @@ fun Route.upsertArticle(repo: ArticleRepository, workgroupRepository: WorkgroupR
) )
} }
post<PostArticleRequest> { post<UpsertArticleRequest> {
val article = call.convertRequestToEntity() val article = call.convertRequestToEntity()
voter.assert { canUpsert(article, citizenOrNull) } voter.assert { canUpsert(article, citizenOrNull) }
val newArticle: ArticleForView = repo.upsert(article) ?: error("Article not updated") 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,15 +13,13 @@ 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
fun Route.authLogin(userRepo: UserRepository) {
post<LoginRequest> { post<LoginRequest> {
try { try {
val credentials = call.receive<UserPasswordCredential>() val credentials = call.receive<UserPasswordCredential>()
@@ -32,4 +30,5 @@ fun Route.authLogin(userRepo: UserRepository) {
call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request") 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,12 +18,12 @@ 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")
class RegisterRequest {
data class Input( data class Input(
val name: Name, val name: Name,
val email: String, val email: String,
@@ -42,11 +42,9 @@ class RegisterRequest {
val plainPassword: String? = null val plainPassword: String? = null
) )
} }
} }
@KtorExperimentalLocationsAPI fun Route.authRegister(citizenRepo: CitizenRepository) {
@KtorExperimentalAPI
fun Route.authRegister(citizenRepo: CitizenRepository) {
fun Input.toCitizen(): Citizen = Citizen( fun Input.toCitizen(): Citizen = Citizen(
name = CitizenI.Name(name.firstName, name.lastName, name.civility), name = CitizenI.Name(name.firstName, name.lastName, name.civility),
birthday = birthday, birthday = birthday,
@@ -69,4 +67,5 @@ fun Route.authRegister(citizenRepo: CitizenRepository) {
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,20 +10,18 @@ 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")
class PasswordlessRequest {
data class Input(val email: String, val url: String) 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
fun Route.authPasswordless(passwordlessAuth: PasswordlessAuth) {
post<PasswordlessRequest> { post<PasswordlessRequest> {
call.receive<Input>().run { call.receive<Input>().run {
try { try {
@@ -34,4 +32,5 @@ fun Route.authPasswordless(passwordlessAuth: PasswordlessAuth) {
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,14 +18,13 @@ import io.ktor.response.respond
import io.ktor.routing.Route import io.ktor.routing.Route
@KtorExperimentalLocationsAPI @KtorExperimentalLocationsAPI
object ChangeMyPassword {
@Location("/citizens/{citizen}/password/change") @Location("/citizens/{citizen}/password/change")
class ChangePasswordCitizenRequest(val citizen: Citizen) { class ChangePasswordCitizenRequest(val citizen: Citizen) {
data class Input(val oldPassword: String, val newPassword: String) data class Input(val oldPassword: String, val newPassword: String)
} }
@KtorExperimentalLocationsAPI fun Route.changeMyPassword(voter: CitizenVoter, userRepository: UserRepository) {
fun Route.changeMyPassword(voter: CitizenVoter, userRepository: UserRepository) {
put<ChangePasswordCitizenRequest> { put<ChangePasswordCitizenRequest> {
voter.assert { canChangePassword(it.citizen, citizenOrNull) } voter.assert { canChangePassword(it.citizen, citizenOrNull) }
try { try {
@@ -44,4 +43,5 @@ fun Route.changeMyPassword(voter: CitizenVoter, userRepository: UserRepository)
call.respond(HttpStatusCode.BadRequest, "Request format is not correct") 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")
class CitizensRequest(
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val sort: String? = null, val sort: String? = null,
val direction: RepositoryI.Direction? = null, val direction: RepositoryI.Direction? = null,
val search: String? = null val search: String? = null
) { ) {
val page: Int = if (page < 1) 1 else page val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit 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,11 +13,11 @@ 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) {
@@ -27,4 +27,5 @@ fun Route.getCurrentCitizen(voter: CitizenVoter) {
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,10 +18,11 @@ 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")
class PostArticleCommentRequest(
val article: ArticleForView val article: ArticleForView
) { ) {
class Comment( class Comment(
val content: String val content: String
) )
@@ -33,10 +34,9 @@ class PostArticleCommentRequest(
content = content content = content
) )
} }
} }
@KtorExperimentalLocationsAPI fun Route.createCommentArticle(repo: CommentArticleRepository, voter: CommentVoter) {
fun Route.createCommentArticle(repo: CommentArticleRepository, voter: CommentVoter) {
post<PostArticleCommentRequest> { post<PostArticleCommentRequest> {
it.getComment(call).let { comment -> it.getComment(call).let { comment ->
voter.assert { canCreate(comment, citizenOrNull) } voter.assert { canCreate(comment, citizenOrNull) }
@@ -44,4 +44,5 @@ fun Route.createCommentArticle(repo: CommentArticleRepository, voter: CommentVot
call.respond(HttpStatusCode.Created, comment) call.respond(HttpStatusCode.Created, comment)
} }
} }
}
} }

View File

@@ -14,21 +14,21 @@ 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")
class ArticleCommentsRequest(
val article: ArticleRef, val article: ArticleRef,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val search: String? = null, val search: String? = null,
sort: String = CommentArticleRepository.Sort.CREATED_AT.sql sort: String = CommentArticleRepository.Sort.CREATED_AT.sql
) { ) {
val page: Int = if (page < 1) 1 else page val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit 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 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()) {
@@ -36,4 +36,5 @@ fun Route.getArticleComments(repo: CommentArticleRepository, voter: CommentVoter
} }
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,14 +19,13 @@ 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 CreateCommentChildrenRequest(val comment: CommentRef) {
class Input(val content: String) class Input(val content: String)
} }
@KtorExperimentalAPI fun Route.createCommentChildren(repo: CommentRepository, voter: CommentVoter) {
@KtorExperimentalLocationsAPI
fun Route.createCommentChildren(repo: CommentRepository, voter: CommentVoter) {
post<CreateCommentChildrenRequest> { post<CreateCommentChildrenRequest> {
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
val newComment = CommentForUpdate( val newComment = CommentForUpdate(
@@ -40,4 +39,5 @@ fun Route.createCommentChildren(repo: CommentRepository, voter: CommentVoter) {
call.respond(HttpStatusCode.Created, newComment) call.respond(HttpStatusCode.Created, newComment)
} }
}
} }

View File

@@ -14,15 +14,13 @@ 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
fun Route.editComment(repo: CommentRepository, voter: CommentVoter) {
put<EditCommentRequest> { put<EditCommentRequest> {
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
voter.assert { canUpdate(comment, citizenOrNull) } voter.assert { canUpdate(comment, citizenOrNull) }
@@ -32,4 +30,5 @@ fun Route.editComment(repo: CommentRepository, voter: CommentVoter) {
call.respond(HttpStatusCode.OK, comment) call.respond(HttpStatusCode.OK, comment)
} }
}
} }

View File

@@ -15,20 +15,19 @@ 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")
class CommentChildrenRequest(
val comment: UUID, val comment: UUID,
page: Int = 1, page: Int = 1,
limit: Int = 50, limit: Int = 50,
val search: String? = null val search: String? = null
) { ) {
val page: Int = if (page < 1) 1 else page val page: Int = if (page < 1) 1 else page
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
} }
@KtorExperimentalAPI fun Route.getChildrenComments(repo: CommentRepository, voter: CommentVoter) {
@KtorExperimentalLocationsAPI
fun Route.getChildrenComments(repo: CommentRepository, voter: CommentVoter) {
get<CommentChildrenRequest> { get<CommentChildrenRequest> {
val comments = val comments =
repo.findByParent( repo.findByParent(
@@ -41,4 +40,5 @@ fun Route.getChildrenComments(repo: CommentRepository, voter: CommentVoter) {
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
fun Route.getOneComment(repo: CommentRepository, voter: CommentVoter) {
get<CommentRequest> { get<CommentRequest> {
val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found") val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found")
voter.assert { canView(comment, citizenOrNull) } 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())
}
}