diff --git a/src/main/kotlin/application/Application.kt b/src/main/kotlin/application/Application.kt index f68cfe5..106adf1 100644 --- a/src/main/kotlin/application/Application.kt +++ b/src/main/kotlin/application/Application.kt @@ -7,28 +7,15 @@ import com.fasterxml.jackson.databind.PropertyNamingStrategy import com.fasterxml.jackson.databind.SerializationFeature import com.fasterxml.jackson.datatype.joda.JodaModule import com.github.jasync.sql.db.postgresql.exceptions.GenericDatabaseException -import component.auth.jwt.jwtInstallation import fr.dcproject.application.Env.PROD -import fr.dcproject.component.article.routes.findArticleVersions -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.article.routes.installArticleRoutes import fr.dcproject.component.auth.ForbiddenException -import fr.dcproject.component.auth.routes.authLogin -import fr.dcproject.component.auth.routes.authPasswordless -import fr.dcproject.component.auth.routes.authRegister +import fr.dcproject.component.auth.jwt.jwtInstallation +import fr.dcproject.component.auth.routes.installAuthRoutes import fr.dcproject.component.auth.user -import fr.dcproject.component.citizen.routes.changeMyPassword -import fr.dcproject.component.citizen.routes.findCitizen -import fr.dcproject.component.citizen.routes.getCurrentCitizen -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.citizen.routes.installCitizenRoutes +import fr.dcproject.component.comment.article.routes.installCommentArticleRoutes +import fr.dcproject.component.comment.generic.routes.installCommentRoutes 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.GetMyFollowsArticle.getMyFollowsArticle @@ -86,14 +73,14 @@ import io.ktor.routing.Routing import io.ktor.server.jetty.EngineMain import io.ktor.util.KtorExperimentalAPI import io.ktor.websocket.WebSockets +import java.time.Duration +import java.util.concurrent.CompletionException import kotlinx.coroutines.ExperimentalCoroutinesApi import org.eclipse.jetty.util.log.Slf4jLog import org.koin.core.qualifier.named import org.koin.ktor.ext.Koin import org.koin.ktor.ext.get import org.slf4j.event.Level -import java.time.Duration -import java.util.concurrent.CompletionException fun main(args: Array): Unit = EngineMain.main(args) @@ -158,30 +145,13 @@ fun Application.module(env: Env = PROD) { install(Routing.Feature) { // trace { application.log.trace(it.buildText()) } + installArticleRoutes() + installAuthRoutes() + installCitizenRoutes() + installCommentArticleRoutes() + installCommentRoutes() + 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 */ getWorkgroups(get(), get()) getWorkgroup(get(), get()) diff --git a/src/main/kotlin/component/article/routes/FindArticleVersions.kt b/src/main/kotlin/component/article/routes/FindArticleVersions.kt index 5fd5c66..38b883e 100644 --- a/src/main/kotlin/component/article/routes/FindArticleVersions.kt +++ b/src/main/kotlin/component/article/routes/FindArticleVersions.kt @@ -14,28 +14,28 @@ import io.ktor.response.respond import io.ktor.routing.Route @KtorExperimentalLocationsAPI -@Location("/articles/{article}/versions") -class ArticleVersionsRequest( - val article: ArticleForView, - page: Int = 1, - limit: Int = 50, - val sort: 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 -} +object FindArticleVersions { + @Location("/articles/{article}/versions") + class ArticleVersionsRequest( + val article: ArticleForView, + page: Int = 1, + limit: Int = 50, + val sort: 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 + } -@KtorExperimentalLocationsAPI -private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) = - findVersionsByVersionId(request.page, request.limit, request.article.versionId) + private fun ArticleRepository.findVersions(request: ArticleVersionsRequest) = + findVersionsByVersionId(request.page, request.limit, request.article.versionId) -@KtorExperimentalLocationsAPI -fun Route.findArticleVersions(repo: ArticleRepository, voter: ArticleVoter) { - get { - repo.findVersions(it) - .apply { voter.assert { canView(it.article, citizenOrNull) } } - .let { call.respond(it) } + fun Route.findArticleVersions(repo: ArticleRepository, voter: ArticleVoter) { + get { + repo.findVersions(it) + .apply { voter.assert { canView(it.article, citizenOrNull) } } + .let { call.respond(it) } + } } } diff --git a/src/main/kotlin/component/article/routes/FindArticles.kt b/src/main/kotlin/component/article/routes/FindArticles.kt index c115220..feb95d2 100644 --- a/src/main/kotlin/component/article/routes/FindArticles.kt +++ b/src/main/kotlin/component/article/routes/FindArticles.kt @@ -8,40 +8,44 @@ import fr.dcproject.voter.assert import fr.postgresjson.connexion.Paginated import fr.postgresjson.repository.RepositoryI import io.ktor.application.call +import io.ktor.locations.KtorExperimentalLocationsAPI import io.ktor.locations.Location import io.ktor.locations.get import io.ktor.response.respond import io.ktor.routing.Route -@Location("/articles") -class ArticlesRequest( - page: Int = 1, - limit: Int = 50, - val sort: String? = null, - val direction: RepositoryI.Direction? = null, - val search: String? = null, - val createdBy: String? = null, - val workgroup: 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 -} +@KtorExperimentalLocationsAPI +object FindArticles { + @Location("/articles") + class ArticlesRequest( + page: Int = 1, + limit: Int = 50, + val sort: String? = null, + val direction: RepositoryI.Direction? = null, + val search: String? = null, + val createdBy: String? = null, + val workgroup: 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 + } -private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated { - return find( - request.page, - request.limit, - request.sort, - request.direction, - request.search, - ArticleRepository.Filter(createdById = request.createdBy, workgroupId = request.workgroup) - ) -} + private fun ArticleRepository.findArticles(request: ArticlesRequest): Paginated { + return find( + request.page, + request.limit, + request.sort, + request.direction, + request.search, + ArticleRepository.Filter(createdById = request.createdBy, workgroupId = request.workgroup) + ) + } -fun Route.findArticles(repo: ArticleRepository, voter: ArticleVoter) { - get { - repo.findArticles(it) - .apply { voter.assert { canView(result, citizenOrNull) } } - .let { call.respond(it) } + fun Route.findArticles(repo: ArticleRepository, voter: ArticleVoter) { + get { + repo.findArticles(it) + .apply { voter.assert { canView(result, citizenOrNull) } } + .let { call.respond(it) } + } } } diff --git a/src/main/kotlin/component/article/routes/GetOneArticle.kt b/src/main/kotlin/component/article/routes/GetOneArticle.kt index 48b311b..f178116 100644 --- a/src/main/kotlin/component/article/routes/GetOneArticle.kt +++ b/src/main/kotlin/component/article/routes/GetOneArticle.kt @@ -4,7 +4,7 @@ import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleRepository import fr.dcproject.component.article.ArticleViewManager 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.dto.CreatedAt import fr.dcproject.dto.Opinionable @@ -19,56 +19,54 @@ import io.ktor.locations.Location import io.ktor.locations.get import io.ktor.response.respond import io.ktor.routing.Route -import io.ktor.util.KtorExperimentalAPI import kotlinx.coroutines.launch import org.koin.core.KoinComponent import org.koin.core.inject import java.util.UUID @KtorExperimentalLocationsAPI -@Location("/articles/{articleId}") -class ArticleRequest(val articleId: UUID) : KoinComponent { - val repo: ArticleRepository by inject() +object GetOneArticle { + @Location("/articles/{articleId}") + 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( - article: ArticleForView, - views: fr.dcproject.entity.ViewAggregation = fr.dcproject.entity.ViewAggregation() - ) : CreatedAt by CreatedAt.Imp(article), - Opinionable by Opinionable.Imp(article), - Votable by Votable.Imp(article), - Versionable by Versionable.Imp(article), - Viewable by Viewable.Imp(views) { - val id = article.id - val title = article.title - val anonymous = article.anonymous - val content = article.content - val description = article.description - val tags = article.tags - val draft = article.draft - val lastVersion = article.lastVersion - val createdBy = article.createdBy - val workgroup = article.workgroup // TODO change to workgroup DTO - } -} - -@KtorExperimentalAPI -@KtorExperimentalLocationsAPI -fun Route.getOneArticle(viewManager: ArticleViewManager, voter: ArticleVoter) { - get { - voter.assert { canView(it.article, citizenOrNull) } - - Output( - it.article, - viewManager.getViewsCount(it.article) - ).also { out -> - call.respond(out) + class Output( + article: ArticleForView, + views: fr.dcproject.entity.ViewAggregation = fr.dcproject.entity.ViewAggregation() + ) : CreatedAt by CreatedAt.Imp(article), + Opinionable by Opinionable.Imp(article), + Votable by Votable.Imp(article), + Versionable by Versionable.Imp(article), + Viewable by Viewable.Imp(views) { + val id = article.id + val title = article.title + val anonymous = article.anonymous + val content = article.content + val description = article.description + val tags = article.tags + val draft = article.draft + val lastVersion = article.lastVersion + val createdBy = article.createdBy + val workgroup = article.workgroup // TODO change to workgroup DTO } + } - launch { - viewManager.addView(call.request.local.remoteHost, it.article, citizenOrNull) + fun Route.getOneArticle(viewManager: ArticleViewManager, voter: ArticleVoter) { + get { + 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) + } } } } diff --git a/src/main/kotlin/component/article/routes/UpsertArticle.kt b/src/main/kotlin/component/article/routes/UpsertArticle.kt index 799e378..67366b2 100644 --- a/src/main/kotlin/component/article/routes/UpsertArticle.kt +++ b/src/main/kotlin/component/article/routes/UpsertArticle.kt @@ -4,7 +4,7 @@ import fr.dcproject.component.article.ArticleForUpdate import fr.dcproject.component.article.ArticleForView import fr.dcproject.component.article.ArticleRepository 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.citizenOrNull import fr.dcproject.component.workgroup.WorkgroupRef @@ -23,47 +23,44 @@ import io.ktor.routing.Route import java.util.UUID @KtorExperimentalLocationsAPI -@Location("/articles") -class PostArticleRequest { - class Input( - val id: UUID?, - val title: String, - val anonymous: Boolean = true, - val content: String, - val description: String, - val tags: List = emptyList(), - val draft: Boolean = false, - val versionId: UUID?, - val workgroup: WorkgroupRef? = null - ) -} - -@KtorExperimentalLocationsAPI -fun Route.upsertArticle(repo: ArticleRepository, workgroupRepository: WorkgroupRepository, voter: ArticleVoter) { - suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receive().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 +object UpsertArticle { + @Location("/articles") + class UpsertArticleRequest { + class Input( + val id: UUID?, + val title: String, + val anonymous: Boolean = true, + val content: String, + val description: String, + val tags: List = emptyList(), + val draft: Boolean = false, + val versionId: UUID?, + val workgroup: WorkgroupRef? = null ) } - post { - val article = call.convertRequestToEntity() + fun Route.upsertArticle(repo: ArticleRepository, workgroupRepository: WorkgroupRepository, voter: ArticleVoter) { + suspend fun ApplicationCall.convertRequestToEntity(): ArticleForUpdate = receive().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) } - - val newArticle: ArticleForView = repo.upsert(article) ?: error("Article not updated") - - call.respond(newArticle) - - raiseEvent(ArticleUpdate.event, ArticleUpdate(newArticle)) + post { + val article = call.convertRequestToEntity() + voter.assert { canUpsert(article, citizenOrNull) } + val newArticle: ArticleForView = repo.upsert(article) ?: error("Article not updated") + call.respond(newArticle) + raiseEvent(ArticleUpdate.event, ArticleUpdate(newArticle)) + } } } diff --git a/src/main/kotlin/component/article/routes/install.kt b/src/main/kotlin/component/article/routes/install.kt new file mode 100644 index 0000000..247949d --- /dev/null +++ b/src/main/kotlin/component/article/routes/install.kt @@ -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()) + } +} diff --git a/src/main/kotlin/component/auth/PasswordlessAuth.kt b/src/main/kotlin/component/auth/PasswordlessAuth.kt index 88c5fee..a73046c 100644 --- a/src/main/kotlin/component/auth/PasswordlessAuth.kt +++ b/src/main/kotlin/component/auth/PasswordlessAuth.kt @@ -6,7 +6,7 @@ import com.sendgrid.helpers.mail.objects.Email import fr.dcproject.component.citizen.CitizenRepository import fr.dcproject.component.citizen.CitizenWithEmail import fr.dcproject.component.citizen.CitizenWithUserI -import fr.dcproject.makeToken +import fr.dcproject.component.auth.jwt.makeToken import fr.dcproject.messages.Mailer import io.ktor.http.URLBuilder diff --git a/src/main/kotlin/component/auth/jwt/JWTMaker.kt b/src/main/kotlin/component/auth/jwt/JWTMaker.kt index 8cf55a4..fe3b8a6 100644 --- a/src/main/kotlin/component/auth/jwt/JWTMaker.kt +++ b/src/main/kotlin/component/auth/jwt/JWTMaker.kt @@ -1,4 +1,4 @@ -package fr.dcproject +package fr.dcproject.component.auth.jwt import com.auth0.jwt.JWT import fr.dcproject.component.auth.UserI diff --git a/src/main/kotlin/component/auth/jwt/JwtInstallation.kt b/src/main/kotlin/component/auth/jwt/JwtInstallation.kt index a7122bd..b878f9a 100644 --- a/src/main/kotlin/component/auth/jwt/JwtInstallation.kt +++ b/src/main/kotlin/component/auth/jwt/JwtInstallation.kt @@ -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.UserRepository -import fr.dcproject.component.auth.jwt.JwtConfig import io.ktor.application.ApplicationCall import io.ktor.auth.Authentication import io.ktor.auth.jwt.jwt diff --git a/src/main/kotlin/component/auth/routes/Login.kt b/src/main/kotlin/component/auth/routes/Login.kt index 3701faf..e117dab 100644 --- a/src/main/kotlin/component/auth/routes/Login.kt +++ b/src/main/kotlin/component/auth/routes/Login.kt @@ -2,7 +2,7 @@ package fr.dcproject.component.auth.routes import com.fasterxml.jackson.databind.exc.MismatchedInputException 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.auth.UserPasswordCredential import io.ktor.http.HttpStatusCode @@ -13,23 +13,22 @@ import io.ktor.request.receive import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.Route -import io.ktor.util.KtorExperimentalAPI @KtorExperimentalLocationsAPI -@Location("/login") -class LoginRequest +object Login { + @Location("/login") + class LoginRequest -@KtorExperimentalLocationsAPI -@KtorExperimentalAPI -fun Route.authLogin(userRepo: UserRepository) { - post { - try { - val credentials = call.receive() - userRepo.findByCredentials(credentials)?.let { user -> - call.respondText(user.makeToken()) - } ?: call.respond(HttpStatusCode.BadRequest, "Username not exist or password is wrong") - } catch (e: MismatchedInputException) { - call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request") + fun Route.authLogin(userRepo: UserRepository) { + post { + try { + val credentials = call.receive() + userRepo.findByCredentials(credentials)?.let { user -> + call.respondText(user.makeToken()) + } ?: call.respond(HttpStatusCode.BadRequest, "Username not exist or password is wrong") + } catch (e: MismatchedInputException) { + call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request") + } } } } diff --git a/src/main/kotlin/component/auth/routes/Register.kt b/src/main/kotlin/component/auth/routes/Register.kt index 23fc92f..659ffe8 100644 --- a/src/main/kotlin/component/auth/routes/Register.kt +++ b/src/main/kotlin/component/auth/routes/Register.kt @@ -3,11 +3,11 @@ package fr.dcproject.component.auth.routes import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException import fr.dcproject.component.auth.User 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.CitizenI 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.features.BadRequestException import io.ktor.http.HttpStatusCode @@ -18,55 +18,54 @@ import io.ktor.request.receive import io.ktor.response.respond import io.ktor.response.respondText import io.ktor.routing.Route -import io.ktor.util.KtorExperimentalAPI import org.joda.time.DateTime @KtorExperimentalLocationsAPI -@Location("/register") -class RegisterRequest { - data class Input( - val name: Name, - val email: String, - val birthday: DateTime, - val voteAnonymous: Boolean = true, - val followAnonymous: Boolean = true, - val user: User - ) { - data class Name( - val firstName: String, - val lastName: String, - val civility: String? = null - ) - data class User( - val username: String, - val plainPassword: String? = null - ) +object Register { + @Location("/register") + class RegisterRequest { + data class Input( + val name: Name, + val email: String, + val birthday: DateTime, + val voteAnonymous: Boolean = true, + val followAnonymous: Boolean = true, + val user: User + ) { + data class Name( + val firstName: String, + val lastName: String, + val civility: String? = null + ) + data class User( + val username: String, + val plainPassword: String? = null + ) + } } -} -@KtorExperimentalLocationsAPI -@KtorExperimentalAPI -fun Route.authRegister(citizenRepo: CitizenRepository) { - fun Input.toCitizen(): Citizen = Citizen( - name = CitizenI.Name(name.firstName, name.lastName, name.civility), - birthday = birthday, - email = email, - followAnonymous = followAnonymous, - voteAnonymous = voteAnonymous, - user = User( - username = user.username, - plainPassword = user.plainPassword, - roles = listOf(UserI.Roles.ROLE_USER) + fun Route.authRegister(citizenRepo: CitizenRepository) { + fun Input.toCitizen(): Citizen = Citizen( + name = CitizenI.Name(name.firstName, name.lastName, name.civility), + birthday = birthday, + email = email, + followAnonymous = followAnonymous, + voteAnonymous = voteAnonymous, + user = User( + username = user.username, + plainPassword = user.plainPassword, + roles = listOf(UserI.Roles.ROLE_USER) + ) ) - ) - post { - try { - val citizen = call.receive().toCitizen() - val createdCitizen = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request") - call.respondText(createdCitizen.makeToken()) - } catch (e: MissingKotlinParameterException) { - call.respond(HttpStatusCode.BadRequest) + post { + try { + val citizen = call.receive().toCitizen() + val createdCitizen = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request") + call.respondText(createdCitizen.makeToken()) + } catch (e: MissingKotlinParameterException) { + call.respond(HttpStatusCode.BadRequest) + } } } } diff --git a/src/main/kotlin/component/auth/routes/Sso.kt b/src/main/kotlin/component/auth/routes/Sso.kt index cbe3ef6..05be7b0 100644 --- a/src/main/kotlin/component/auth/routes/Sso.kt +++ b/src/main/kotlin/component/auth/routes/Sso.kt @@ -1,7 +1,7 @@ package fr.dcproject.component.auth.routes 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.http.HttpStatusCode import io.ktor.locations.KtorExperimentalLocationsAPI @@ -10,28 +10,27 @@ import io.ktor.locations.post import io.ktor.request.receive import io.ktor.response.respond import io.ktor.routing.Route -import io.ktor.util.KtorExperimentalAPI @KtorExperimentalLocationsAPI -@Location("/auth/passwordless") -class PasswordlessRequest { - data class Input(val email: String, val url: String) -} +object Sso { + @Location("/auth/passwordless") + class PasswordlessRequest { + data class Input(val email: String, val url: String) + } -/** - * Send an email to the citizen with a link to automatically connect - */ -@KtorExperimentalLocationsAPI -@KtorExperimentalAPI -fun Route.authPasswordless(passwordlessAuth: PasswordlessAuth) { - post { - call.receive().run { - try { - passwordlessAuth.sendEmail(email, url) - } catch (e: PasswordlessAuth.EmailNotFound) { - call.respond(HttpStatusCode.NotFound) + /** + * Send an email to the citizen with a link to automatically connect + */ + fun Route.authPasswordless(passwordlessAuth: PasswordlessAuth) { + post { + call.receive().run { + try { + passwordlessAuth.sendEmail(email, url) + } catch (e: PasswordlessAuth.EmailNotFound) { + call.respond(HttpStatusCode.NotFound) + } + call.respond(HttpStatusCode.NoContent) } - call.respond(HttpStatusCode.NoContent) } } } diff --git a/src/main/kotlin/component/auth/routes/install.kt b/src/main/kotlin/component/auth/routes/install.kt new file mode 100644 index 0000000..fcac8a5 --- /dev/null +++ b/src/main/kotlin/component/auth/routes/install.kt @@ -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()) + } +} diff --git a/src/main/kotlin/component/citizen/routes/ChangeMyPassword.kt b/src/main/kotlin/component/citizen/routes/ChangeMyPassword.kt index 78d9ba4..d4b8e14 100644 --- a/src/main/kotlin/component/citizen/routes/ChangeMyPassword.kt +++ b/src/main/kotlin/component/citizen/routes/ChangeMyPassword.kt @@ -18,30 +18,30 @@ import io.ktor.response.respond import io.ktor.routing.Route @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") -class ChangePasswordCitizenRequest(val citizen: Citizen) { - data class Input(val oldPassword: String, val newPassword: String) -} + fun Route.changeMyPassword(voter: CitizenVoter, userRepository: UserRepository) { + put { + voter.assert { canChangePassword(it.citizen, citizenOrNull) } + try { + val content = call.receive() + 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 -fun Route.changeMyPassword(voter: CitizenVoter, userRepository: UserRepository) { - put { - voter.assert { canChangePassword(it.citizen, citizenOrNull) } - try { - val content = call.receive() - 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) + call.respond(HttpStatusCode.Created) + } + } catch (e: MissingKotlinParameterException) { + call.respond(HttpStatusCode.BadRequest, "Request format is not correct") } - } catch (e: MissingKotlinParameterException) { - call.respond(HttpStatusCode.BadRequest, "Request format is not correct") } } } diff --git a/src/main/kotlin/component/citizen/routes/FindCitizens.kt b/src/main/kotlin/component/citizen/routes/FindCitizens.kt index 987893f..baadc20 100644 --- a/src/main/kotlin/component/citizen/routes/FindCitizens.kt +++ b/src/main/kotlin/component/citizen/routes/FindCitizens.kt @@ -13,23 +13,24 @@ import io.ktor.response.respond import io.ktor.routing.Route @KtorExperimentalLocationsAPI -@Location("/citizens") -class CitizensRequest( - page: Int = 1, - limit: Int = 50, - val sort: 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 -} +object FindCitizens { + @Location("/citizens") + class CitizensRequest( + page: Int = 1, + limit: Int = 50, + val sort: 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 + } -@KtorExperimentalLocationsAPI -fun Route.findCitizen(voter: CitizenVoter, repo: CitizenRepository) { - get { - val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) - voter.assert { canView(citizens.result, citizenOrNull) } - call.respond(citizens) + fun Route.findCitizen(voter: CitizenVoter, repo: CitizenRepository) { + get { + val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search) + voter.assert { canView(citizens.result, citizenOrNull) } + call.respond(citizens) + } } } diff --git a/src/main/kotlin/component/citizen/routes/GetCurrentCitizen.kt b/src/main/kotlin/component/citizen/routes/GetCurrentCitizen.kt index 1e828e0..a0b9e3c 100644 --- a/src/main/kotlin/component/citizen/routes/GetCurrentCitizen.kt +++ b/src/main/kotlin/component/citizen/routes/GetCurrentCitizen.kt @@ -13,18 +13,19 @@ import io.ktor.response.respond import io.ktor.routing.Route @KtorExperimentalLocationsAPI -@Location("/citizens/current") -class CurrentCitizenRequest +object GetCurrentCitizen { + @Location("/citizens/current") + class CurrentCitizenRequest -@KtorExperimentalLocationsAPI -fun Route.getCurrentCitizen(voter: CitizenVoter) { - get { - val currentUser = citizenOrNull - if (currentUser === null) { - call.respond(HttpStatusCode.Unauthorized) - } else { - voter.assert { canView(currentUser, citizenOrNull) } - call.respond(citizen) + fun Route.getCurrentCitizen(voter: CitizenVoter) { + get { + val currentUser = citizenOrNull + if (currentUser === null) { + call.respond(HttpStatusCode.Unauthorized) + } else { + voter.assert { canView(currentUser, citizenOrNull) } + call.respond(citizen) + } } } } diff --git a/src/main/kotlin/component/citizen/routes/GetOneCitizen.kt b/src/main/kotlin/component/citizen/routes/GetOneCitizen.kt index 1276aaf..ea52c67 100644 --- a/src/main/kotlin/component/citizen/routes/GetOneCitizen.kt +++ b/src/main/kotlin/component/citizen/routes/GetOneCitizen.kt @@ -12,14 +12,15 @@ import io.ktor.response.respond import io.ktor.routing.Route @KtorExperimentalLocationsAPI -@Location("/citizens/{citizen}") -class CitizenRequest(val citizen: Citizen) +object GetOneCitizen { + @Location("/citizens/{citizen}") + class CitizenRequest(val citizen: Citizen) -@KtorExperimentalLocationsAPI -fun Route.getOneCitizen(voter: CitizenVoter) { - get { - voter.assert { canView(it.citizen, citizenOrNull) } + fun Route.getOneCitizen(voter: CitizenVoter) { + get { + voter.assert { canView(it.citizen, citizenOrNull) } - call.respond(it.citizen) + call.respond(it.citizen) + } } } diff --git a/src/main/kotlin/component/citizen/routes/install.kt b/src/main/kotlin/component/citizen/routes/install.kt new file mode 100644 index 0000000..865ee07 --- /dev/null +++ b/src/main/kotlin/component/citizen/routes/install.kt @@ -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()) + } +} diff --git a/src/main/kotlin/component/comment/article/routes/CreateCommentArticle.kt b/src/main/kotlin/component/comment/article/routes/CreateCommentArticle.kt index 48333bb..4080cfd 100644 --- a/src/main/kotlin/component/comment/article/routes/CreateCommentArticle.kt +++ b/src/main/kotlin/component/comment/article/routes/CreateCommentArticle.kt @@ -18,30 +18,31 @@ import io.ktor.response.respond import io.ktor.routing.Route @KtorExperimentalLocationsAPI -@Location("/articles/{article}/comments") -class PostArticleCommentRequest( - val article: ArticleForView -) { - class Comment( - val content: String - ) - - suspend fun getComment(call: ApplicationCall) = call.receive().run { - CommentForUpdate( - target = article, - createdBy = call.citizen, - content = content +object CreateCommentArticle { + @Location("/articles/{article}/comments") + class PostArticleCommentRequest( + val article: ArticleForView + ) { + class Comment( + val content: String ) - } -} -@KtorExperimentalLocationsAPI -fun Route.createCommentArticle(repo: CommentArticleRepository, voter: CommentVoter) { - post { - it.getComment(call).let { comment -> - voter.assert { canCreate(comment, citizenOrNull) } - repo.comment(comment) - call.respond(HttpStatusCode.Created, comment) + suspend fun getComment(call: ApplicationCall) = call.receive().run { + CommentForUpdate( + target = article, + createdBy = call.citizen, + content = content + ) + } + } + + fun Route.createCommentArticle(repo: CommentArticleRepository, voter: CommentVoter) { + post { + it.getComment(call).let { comment -> + voter.assert { canCreate(comment, citizenOrNull) } + repo.comment(comment) + call.respond(HttpStatusCode.Created, comment) + } } } } diff --git a/src/main/kotlin/component/comment/article/routes/GetArticleComments.kt b/src/main/kotlin/component/comment/article/routes/GetArticleComments.kt index 7e2d718..5b46ed2 100644 --- a/src/main/kotlin/component/comment/article/routes/GetArticleComments.kt +++ b/src/main/kotlin/component/comment/article/routes/GetArticleComments.kt @@ -14,26 +14,27 @@ import io.ktor.response.respond import io.ktor.routing.Route @KtorExperimentalLocationsAPI -@Location("/articles/{article}/comments") -class ArticleCommentsRequest( - val article: ArticleRef, - page: Int = 1, - limit: Int = 50, - 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 sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.fromString(sort) ?: CommentArticleRepository.Sort.CREATED_AT -} +object GetArticleComments { + @Location("/articles/{article}/comments") + class ArticleCommentsRequest( + val article: ArticleRef, + page: Int = 1, + limit: Int = 50, + 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 sort: CommentArticleRepository.Sort = CommentArticleRepository.Sort.fromString(sort) ?: CommentArticleRepository.Sort.CREATED_AT + } -@KtorExperimentalLocationsAPI -fun Route.getArticleComments(repo: CommentArticleRepository, voter: CommentVoter) { - get { - val comment = repo.findByTarget(it.article, it.page, it.limit, it.sort) - if (comment.result.isNotEmpty()) { - voter.assert { canView(comment.result, citizenOrNull) } + fun Route.getArticleComments(repo: CommentArticleRepository, voter: CommentVoter) { + get { + val comment = repo.findByTarget(it.article, it.page, it.limit, it.sort) + if (comment.result.isNotEmpty()) { + voter.assert { canView(comment.result, citizenOrNull) } + } + call.respond(HttpStatusCode.OK, comment) } - call.respond(HttpStatusCode.OK, comment) } } diff --git a/src/main/kotlin/component/comment/article/routes/GetCitizenArticleComments.kt b/src/main/kotlin/component/comment/article/routes/GetCitizenArticleComments.kt index 2b108a4..349af1f 100644 --- a/src/main/kotlin/component/comment/article/routes/GetCitizenArticleComments.kt +++ b/src/main/kotlin/component/comment/article/routes/GetCitizenArticleComments.kt @@ -13,15 +13,16 @@ import io.ktor.response.respond import io.ktor.routing.Route @KtorExperimentalLocationsAPI -@Location("/citizens/{citizen}/comments/articles") -class CitizenCommentArticleRequest(val citizen: Citizen) +object GetCitizenArticleComments { + @Location("/citizens/{citizen}/comments/articles") + class CitizenCommentArticleRequest(val citizen: Citizen) -@KtorExperimentalLocationsAPI -fun Route.getCitizenArticleComments(repo: CommentArticleRepository, voter: CommentVoter) { - get { - repo.findByCitizen(it.citizen).let { comments -> - voter.assert { canView(comments.result, citizenOrNull) } - call.respond(comments) + fun Route.getCitizenArticleComments(repo: CommentArticleRepository, voter: CommentVoter) { + get { + repo.findByCitizen(it.citizen).let { comments -> + voter.assert { canView(comments.result, citizenOrNull) } + call.respond(comments) + } } } } diff --git a/src/main/kotlin/component/comment/article/routes/install.kt b/src/main/kotlin/component/comment/article/routes/install.kt new file mode 100644 index 0000000..b161c80 --- /dev/null +++ b/src/main/kotlin/component/comment/article/routes/install.kt @@ -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()) + } +} diff --git a/src/main/kotlin/component/comment/generic/routes/CreateCommentChildrenRequest.kt b/src/main/kotlin/component/comment/generic/routes/CreateCommentChildren.kt similarity index 50% rename from src/main/kotlin/component/comment/generic/routes/CreateCommentChildrenRequest.kt rename to src/main/kotlin/component/comment/generic/routes/CreateCommentChildren.kt index a539ba4..bb3742b 100644 --- a/src/main/kotlin/component/comment/generic/routes/CreateCommentChildrenRequest.kt +++ b/src/main/kotlin/component/comment/generic/routes/CreateCommentChildren.kt @@ -19,25 +19,25 @@ import io.ktor.routing.Route import io.ktor.util.KtorExperimentalAPI @KtorExperimentalLocationsAPI -@Location("/comments/{comment}/children") -class CreateCommentChildrenRequest(val comment: CommentRef) { - class Input(val content: String) -} +object CreateCommentChildren { + @Location("/comments/{comment}/children") + class CreateCommentChildrenRequest(val comment: CommentRef) { + class Input(val content: String) + } -@KtorExperimentalAPI -@KtorExperimentalLocationsAPI -fun Route.createCommentChildren(repo: CommentRepository, voter: CommentVoter) { - post { - val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") - val newComment = CommentForUpdate( - content = call.receive().content, - createdBy = citizen, - parent = parent - ) + fun Route.createCommentChildren(repo: CommentRepository, voter: CommentVoter) { + post { + val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") + val newComment = CommentForUpdate( + content = call.receive().content, + createdBy = citizen, + parent = parent + ) - voter.assert { canCreate(newComment, citizenOrNull) } - repo.comment(newComment) + voter.assert { canCreate(newComment, citizenOrNull) } + repo.comment(newComment) - call.respond(HttpStatusCode.Created, newComment) + call.respond(HttpStatusCode.Created, newComment) + } } } diff --git a/src/main/kotlin/component/comment/generic/routes/EditComment.kt b/src/main/kotlin/component/comment/generic/routes/EditComment.kt index 84392c5..86862b8 100644 --- a/src/main/kotlin/component/comment/generic/routes/EditComment.kt +++ b/src/main/kotlin/component/comment/generic/routes/EditComment.kt @@ -14,22 +14,21 @@ import io.ktor.locations.put import io.ktor.request.receiveText import io.ktor.response.respond import io.ktor.routing.Route -import io.ktor.util.KtorExperimentalAPI @KtorExperimentalLocationsAPI -@Location("/comments/{comment}") -class EditCommentRequest(val comment: CommentRef) +object EditComment { + @Location("/comments/{comment}") + class EditCommentRequest(val comment: CommentRef) -@KtorExperimentalAPI -@KtorExperimentalLocationsAPI -fun Route.editComment(repo: CommentRepository, voter: CommentVoter) { - put { - val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") - voter.assert { canUpdate(comment, citizenOrNull) } + fun Route.editComment(repo: CommentRepository, voter: CommentVoter) { + put { + val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found") + voter.assert { canUpdate(comment, citizenOrNull) } - comment.content = call.receiveText() - repo.edit(comment) + comment.content = call.receiveText() + repo.edit(comment) - call.respond(HttpStatusCode.OK, comment) + call.respond(HttpStatusCode.OK, comment) + } } } diff --git a/src/main/kotlin/component/comment/generic/routes/GetCommentChildrenRequest.kt b/src/main/kotlin/component/comment/generic/routes/GetCommentChildrenRequest.kt index 3a7bfaf..76de571 100644 --- a/src/main/kotlin/component/comment/generic/routes/GetCommentChildrenRequest.kt +++ b/src/main/kotlin/component/comment/generic/routes/GetCommentChildrenRequest.kt @@ -15,30 +15,30 @@ import io.ktor.util.KtorExperimentalAPI import java.util.UUID @KtorExperimentalLocationsAPI -@Location("/comments/{comment}/children") -class CommentChildrenRequest( - val comment: UUID, - page: Int = 1, - 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 -} +object GetCommentChildren { + @Location("/comments/{comment}/children") + class CommentChildrenRequest( + val comment: UUID, + page: Int = 1, + 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 + } -@KtorExperimentalAPI -@KtorExperimentalLocationsAPI -fun Route.getChildrenComments(repo: CommentRepository, voter: CommentVoter) { - get { - val comments = - repo.findByParent( - it.comment, - it.page, - it.limit - ) + fun Route.getChildrenComments(repo: CommentRepository, voter: CommentVoter) { + get { + val comments = + repo.findByParent( + it.comment, + 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) + } } } diff --git a/src/main/kotlin/component/comment/generic/routes/GetOneComment.kt b/src/main/kotlin/component/comment/generic/routes/GetOneComment.kt index 82608d7..3513ce6 100644 --- a/src/main/kotlin/component/comment/generic/routes/GetOneComment.kt +++ b/src/main/kotlin/component/comment/generic/routes/GetOneComment.kt @@ -16,16 +16,16 @@ import io.ktor.routing.Route import io.ktor.util.KtorExperimentalAPI @KtorExperimentalLocationsAPI -@Location("/comments/{comment}") -class CommentRequest(val comment: CommentRef) +object GetOneComment { + @Location("/comments/{comment}") + class CommentRequest(val comment: CommentRef) -@KtorExperimentalAPI -@KtorExperimentalLocationsAPI -fun Route.getOneComment(repo: CommentRepository, voter: CommentVoter) { - get { - val comment = repo.findById(it.comment.id) ?: throw NotFoundException("Comment ${it.comment.id} not found") - voter.assert { canView(comment, citizenOrNull) } + fun Route.getOneComment(repo: CommentRepository, voter: CommentVoter) { + get { + 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) + } } } diff --git a/src/main/kotlin/component/comment/generic/routes/install.kt b/src/main/kotlin/component/comment/generic/routes/install.kt new file mode 100644 index 0000000..8ea983c --- /dev/null +++ b/src/main/kotlin/component/comment/generic/routes/install.kt @@ -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()) + } +}