Refactors Citizen into component

Refactor CitizenVoter
Split citizens routes
This commit is contained in:
2021-01-14 15:07:59 +01:00
parent a1c1accc87
commit 6a8c5bf717
63 changed files with 404 additions and 346 deletions

View File

@@ -12,6 +12,10 @@ import fr.dcproject.component.article.route.findArticleVersions
import fr.dcproject.component.article.route.upsertArticle
import fr.dcproject.component.article.routes.findArticles
import fr.dcproject.component.article.routes.getOneArticle
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.elasticsearch.configElasticIndexes
import fr.dcproject.entity.User
import fr.dcproject.event.EventNotification
@@ -71,7 +75,6 @@ fun Application.module(env: Env = PROD) {
install(AuthorizationVoter) {
voters = listOf(
ConstitutionVoter(),
CitizenVoter(),
CommentVoter(),
VoteVoter(),
FollowVoter(),
@@ -151,12 +154,18 @@ fun Application.module(env: Env = PROD) {
install(Routing.Feature) {
// trace { application.log.trace(it.buildText()) }
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())
auth(get(), get(), get())
citizen(get(), get())
constitution(get())
followArticle(get())
followConstitution(get())

View File

@@ -1,5 +1,6 @@
package fr.dcproject
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import io.ktor.application.*
@@ -7,8 +8,7 @@ import io.ktor.auth.*
import io.ktor.util.*
import io.ktor.util.pipeline.*
import org.koin.core.context.GlobalContext
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.component.citizen.Citizen as CitizenEntity
class ForbiddenException(message: String) : Exception(message)

View File

@@ -3,7 +3,13 @@ package fr.dcproject
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.CommentRef
import fr.dcproject.entity.Constitution
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.entity.WorkgroupRef
import fr.dcproject.repository.OpinionChoice
import fr.dcproject.repository.Workgroup
import io.ktor.features.*
@@ -78,7 +84,7 @@ val converters: ConverterDeclaration = {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<fr.dcproject.repository.Citizen>().findById(id) ?: throw NotFoundException("Citizen $values not found")
get<fr.dcproject.component.citizen.CitizenRepository>().findById(id) ?: throw NotFoundException("Citizen $values not found")
}
}

View File

@@ -11,6 +11,8 @@ import com.rabbitmq.client.ConnectionFactory
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.component.citizen.CitizenVoter
import fr.dcproject.event.publisher.Publisher
import fr.dcproject.messages.Mailer
import fr.dcproject.messages.NotificationEmailSender
@@ -27,7 +29,6 @@ import org.apache.http.HttpHost
import org.elasticsearch.client.RestClient
import org.koin.core.qualifier.named
import org.koin.dsl.module
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.CommentArticle as CommentArticleRepository
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository
import fr.dcproject.repository.CommentGeneric as CommentGenericRepository
@@ -117,6 +118,7 @@ val KoinModule = module {
// Voters
single { ArticleVoter(get()) }
single { CitizenVoter() }
// Elasticsearch Client
single<RestClient> {

View File

@@ -1,5 +1,6 @@
package fr.dcproject.component.article
import fr.dcproject.component.citizen.*
import fr.dcproject.entity.*
import fr.postgresjson.entity.*
import org.joda.time.DateTime

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.article
import fr.dcproject.entity.CitizenI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.ViewAggregation
import fr.dcproject.utils.contentToString
import fr.dcproject.utils.getJsonField

View File

@@ -1,6 +1,6 @@
package fr.dcproject.component.article
import fr.dcproject.entity.CitizenI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.CreatedBy
import fr.dcproject.entity.VersionableRef
import fr.dcproject.voter.Voter

View File

@@ -1,6 +1,10 @@
package fr.dcproject.entity
package fr.dcproject.component.citizen
import fr.dcproject.entity.CitizenI.Name
import fr.dcproject.component.citizen.CitizenI.Name
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.entity.UserRef
import fr.dcproject.entity.WorkgroupSimple
import fr.postgresjson.entity.*
import org.joda.time.DateTime
import java.util.*

View File

@@ -0,0 +1,49 @@
package fr.dcproject.component.citizen
import fr.dcproject.entity.UserI
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import net.pearx.kasechange.toSnakeCase
import java.util.*
class CitizenRepository(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): Citizen? = requester
.getFunction("find_citizen_by_id_with_user_and_workgroups")
.selectOne("id" to id)
fun findByUser(user: UserI): Citizen? = requester
.getFunction("find_citizen_by_user_id")
.selectOne("user_id" to user.id)
fun findByUsername(unsername: String): Citizen? = requester
.getFunction("find_citizen_by_username")
.selectOne("username" to unsername)
fun findByEmail(email: String): Citizen? = requester
.getFunction("find_citizen_by_email")
.selectOne("email" to email)
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: RepositoryI.Direction? = null,
search: String? = null
): Paginated<CitizenBasic> = requester
.getFunction("find_citizens")
.select(
page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search
)
fun upsert(citizen: CitizenFull): Citizen? = requester
.getFunction("upsert_citizen")
.selectOne("resource" to citizen)
fun insertWithUser(citizen: CitizenFull): Citizen? = requester
.getFunction("insert_citizen_with_user")
.selectOne("resource" to citizen)
}

View File

@@ -0,0 +1,26 @@
package fr.dcproject.component.citizen
import fr.dcproject.voter.Voter
import fr.dcproject.voter.VoterResponse
import fr.postgresjson.entity.EntityDeletedAt
class CitizenVoter : Voter() {
fun <S> canView(subjects: List<S>, connectedCitizen: CitizenI?): VoterResponse where S : CitizenI, S: EntityDeletedAt =
canAll(subjects) { canView(it, connectedCitizen) }
fun <S> canView(subject: S, connectedCitizen: CitizenI?): VoterResponse where S : CitizenI, S: EntityDeletedAt {
if (connectedCitizen == null) return denied("You must be connected to view citizen", "citizen.view.connected")
return if (subject.isDeleted()) denied("You cannot view a deleted citizen", "citizen.view.deleted")
else granted()
}
fun <S: CitizenI> canUpdate(subject: S, connectedCitizen: CitizenI?): VoterResponse {
if (connectedCitizen == null) return denied("You must be connected to update Citizen", "citizen.update.notConnected")
return if (subject.id == connectedCitizen.id) granted() else denied("You can only update your citizen", "citizen.update.notYours")
}
fun <S: CitizenI> canChangePassword(subject: S, connectedCitizen: CitizenI?): VoterResponse {
if (connectedCitizen == null) return denied("You must be connected to change your password", "citizen.changePassword.notConnected")
return if (subject.id == connectedCitizen.id) granted() else denied("You can only change your password", "citizen.password.notYours")
}
}

View File

@@ -0,0 +1,45 @@
package fr.dcproject.component.citizen.routes
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenVoter
import fr.dcproject.repository.User
import fr.dcproject.voter.assert
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
@KtorExperimentalLocationsAPI
@Location("/citizens/{citizen}/password/change")
class ChangePasswordCitizenRequest(val citizen: Citizen) {
data class Input(val oldPassword: String, val newPassword: String)
}
@KtorExperimentalLocationsAPI
fun Route.changeMyPassword(voter: CitizenVoter, userRepository: User) {
put<ChangePasswordCitizenRequest> {
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)
call.respond(HttpStatusCode.Created)
}
} catch (e: MissingKotlinParameterException) {
call.respond(HttpStatusCode.BadRequest, "Request format is not correct")
}
}
}

View File

@@ -0,0 +1,33 @@
package fr.dcproject.component.citizen.routes
import fr.dcproject.citizenOrNull
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.component.citizen.CitizenVoter
import fr.dcproject.voter.assert
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
@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
}
@KtorExperimentalLocationsAPI
fun Route.findCitizen(voter: CitizenVoter, repo: CitizenRepository) {
get<CitizensRequest> {
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
voter.assert { canView(citizens.result, citizenOrNull) }
call.respond(citizens)
}
}

View File

@@ -0,0 +1,28 @@
package fr.dcproject.component.citizen.routes
import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.component.citizen.CitizenVoter
import fr.dcproject.voter.assert
import io.ktor.application.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
@KtorExperimentalLocationsAPI
@Location("/citizens/current")
class CurrentCitizenRequest
@KtorExperimentalLocationsAPI
fun Route.getCurrentCitizen(voter: CitizenVoter) {
get<CurrentCitizenRequest> {
val currentUser = citizenOrNull
if (currentUser === null) {
call.respond(HttpStatusCode.Unauthorized)
} else {
voter.assert { canView(currentUser, citizenOrNull) }
call.respond(citizen)
}
}
}

View File

@@ -0,0 +1,23 @@
package fr.dcproject.component.citizen.routes
import fr.dcproject.citizenOrNull
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenVoter
import fr.dcproject.voter.assert
import io.ktor.application.*
import io.ktor.locations.*
import io.ktor.response.*
import io.ktor.routing.*
@KtorExperimentalLocationsAPI
@Location("/citizens/{citizen}")
class CitizenRequest(val citizen: Citizen)
@KtorExperimentalLocationsAPI
fun Route.getOneCitizen(voter: CitizenVoter) {
get<CitizenRequest> {
voter.assert { canView(it.citizen, citizenOrNull) }
call.respond(it.citizen)
}
}

View File

@@ -1,5 +1,6 @@
package fr.dcproject.entity
import fr.dcproject.component.citizen.CitizenRef
import fr.postgresjson.entity.*
import org.joda.time.DateTime
import java.util.*

View File

@@ -2,6 +2,8 @@ package fr.dcproject.entity
import fr.dcproject.component.article.ArticleI
import fr.dcproject.component.article.ArticleSimple
import fr.dcproject.component.citizen.CitizenSimple
import fr.dcproject.component.citizen.CitizenWithUserI
import fr.postgresjson.entity.*
import java.util.*

View File

@@ -1,5 +1,6 @@
package fr.dcproject.entity
import fr.dcproject.component.citizen.CitizenI
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.EntityI

View File

@@ -1,6 +1,7 @@
package fr.dcproject.entity
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.CitizenI
import fr.postgresjson.entity.EntityCreatedAt
import fr.postgresjson.entity.EntityCreatedBy
import fr.postgresjson.entity.UuidEntity

View File

@@ -1,5 +1,8 @@
package fr.dcproject.entity
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenI
import fr.postgresjson.entity.*
import java.util.*

View File

@@ -1,6 +1,10 @@
package fr.dcproject.entity
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRef
import fr.postgresjson.entity.*
import java.util.*

View File

@@ -1,5 +1,8 @@
package fr.dcproject.entity
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenI
import fr.postgresjson.entity.*
import java.util.*
@Deprecated("")

View File

@@ -1,5 +1,8 @@
package fr.dcproject.entity
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenWithUserI
import fr.dcproject.entity.WorkgroupWithMembersI.Member
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role
import fr.postgresjson.entity.*

View File

@@ -4,7 +4,7 @@ import com.rabbitmq.client.*
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
import fr.dcproject.Config
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.CitizenRef
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.FollowSimple
import fr.dcproject.entity.TargetRef
import fr.dcproject.event.publisher.Publisher

View File

@@ -5,13 +5,13 @@ import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.article.ArticleWithTitleI
import fr.dcproject.entity.CitizenBasicI
import fr.dcproject.entity.CitizenRef
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.FollowSimple
import fr.dcproject.entity.TargetRef
import fr.postgresjson.entity.UuidEntityI
import java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository
class NotificationEmailSender(
private val mailer: Mailer,

View File

@@ -4,9 +4,9 @@ import com.sendgrid.helpers.mail.Mail
import com.sendgrid.helpers.mail.objects.Content
import com.sendgrid.helpers.mail.objects.Email
import fr.dcproject.JwtConfig
import fr.dcproject.entity.CitizenBasicI
import io.ktor.http.URLBuilder
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.component.citizen.CitizenBasicI
import fr.dcproject.component.citizen.CitizenRepository
import io.ktor.http.*
class SsoManager(
private val mailer: Mailer,

View File

@@ -1,53 +0,0 @@
package fr.dcproject.repository
import fr.dcproject.entity.CitizenBasic
import fr.dcproject.entity.CitizenFull
import fr.dcproject.entity.UserI
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
class Citizen(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): CitizenEntity? = requester
.getFunction("find_citizen_by_id_with_user_and_workgroups")
.selectOne("id" to id)
fun findByUser(user: UserI): CitizenEntity? = requester
.getFunction("find_citizen_by_user_id")
.selectOne("user_id" to user.id)
fun findByUsername(unsername: String): CitizenEntity? = requester
.getFunction("find_citizen_by_username")
.selectOne("username" to unsername)
fun findByEmail(email: String): CitizenEntity? = requester
.getFunction("find_citizen_by_email")
.selectOne("email" to email)
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: Direction? = null,
search: String? = null
): Paginated<CitizenBasic> = requester
.getFunction("find_citizens")
.select(
page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search
)
fun upsert(citizen: CitizenFull): CitizenEntity? = requester
.getFunction("upsert_citizen")
.selectOne("resource" to citizen)
fun insertWithUser(citizen: CitizenFull): CitizenEntity? = requester
.getFunction("insert_citizen_with_user")
.selectOne("resource" to citizen)
}

View File

@@ -2,6 +2,8 @@ package fr.dcproject.repository
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.*
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester

View File

@@ -1,7 +1,7 @@
package fr.dcproject.repository
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.CitizenWithUserI
import fr.dcproject.component.citizen.CitizenWithUserI
import fr.dcproject.entity.ConstitutionSimple
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester

View File

@@ -2,7 +2,12 @@ package fr.dcproject.repository
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.entity.FollowForUpdate
import fr.dcproject.entity.FollowSimple
import fr.dcproject.entity.TargetRef
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.UuidEntity

View File

@@ -2,7 +2,7 @@ package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.CitizenRef
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.entity.OpinionForUpdate
import fr.dcproject.entity.TargetRef
@@ -11,7 +11,7 @@ import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.component.citizen.Citizen as CitizenEntity
import fr.dcproject.entity.Opinion as OpinionEntity
import fr.dcproject.entity.OpinionArticle as OpinionArticleEntity
import fr.dcproject.entity.OpinionChoice as OpinionChoiceEntity

View File

@@ -2,13 +2,14 @@ package fr.dcproject.repository
import com.fasterxml.jackson.core.type.TypeReference
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.*
import fr.dcproject.entity.Constitution
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.repository.RepositoryI
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.component.citizen.Citizen as CitizenEntity
import fr.dcproject.entity.Vote as VoteEntity
open class Vote<T : TargetI>(override var requester: Requester) : RepositoryI {

View File

@@ -1,6 +1,11 @@
package fr.dcproject.repository
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.WorkgroupI
import fr.dcproject.entity.WorkgroupRef
import fr.dcproject.entity.WorkgroupSimple
import fr.dcproject.entity.WorkgroupWithMembersI
import fr.dcproject.entity.WorkgroupWithMembersI.Member
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester

View File

@@ -3,25 +3,22 @@ package fr.dcproject.routes
import com.fasterxml.jackson.databind.exc.MismatchedInputException
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.JwtConfig
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.UserI.Roles.ROLE_USER
import fr.dcproject.messages.SsoManager
import fr.dcproject.routes.AuthPaths.LoginRequest
import fr.dcproject.routes.AuthPaths.RegisterRequest
import fr.dcproject.routes.AuthPaths.SsoRequest
import io.ktor.application.call
import io.ktor.auth.UserPasswordCredential
import io.ktor.features.BadRequestException
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.post
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 fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.repository.Citizen as CitizenRepository
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.features.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import io.ktor.util.*
import fr.dcproject.component.citizen.Citizen as CitizenEntity
import fr.dcproject.repository.User as UserRepository
@KtorExperimentalLocationsAPI

View File

@@ -1,96 +0,0 @@
package fr.dcproject.routes
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
import fr.dcproject.citizen
import fr.dcproject.citizenOrNull
import fr.dcproject.entity.Citizen
import fr.dcproject.routes.CitizenPaths.ChangePasswordCitizenRequest
import fr.dcproject.routes.CitizenPaths.CitizenRequest
import fr.dcproject.routes.CitizenPaths.CitizensRequest
import fr.dcproject.routes.CitizenPaths.CurrentCitizenRequest
import fr.dcproject.security.voter.CitizenVoter.Action.CHANGE_PASSWORD
import fr.dcproject.security.voter.CitizenVoter.Action.VIEW
import fr.ktorVoter.assertCan
import fr.ktorVoter.assertCanAll
import fr.postgresjson.repository.RepositoryI.Direction
import io.ktor.application.*
import io.ktor.auth.*
import io.ktor.http.*
import io.ktor.locations.*
import io.ktor.request.*
import io.ktor.response.*
import io.ktor.routing.*
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.User as UserRepository
@KtorExperimentalLocationsAPI
object CitizenPaths {
@Location("/citizens")
class CitizensRequest(
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: 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
}
@Location("/citizens/{citizen}")
class CitizenRequest(val citizen: Citizen)
@Location("/citizens/current")
class CurrentCitizenRequest
@Location("/citizens/{citizen}/password/change")
class ChangePasswordCitizenRequest(val citizen: Citizen) {
data class Content(val oldPassword: String, val newPassword: String)
}
}
@KtorExperimentalLocationsAPI
fun Route.citizen(
repo: CitizenRepository,
userRepository: UserRepository
) {
get<CitizensRequest> {
val citizens = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
assertCanAll(VIEW, citizens.result)
call.respond(citizens)
}
get<CitizenRequest> {
assertCan(VIEW, it.citizen)
call.respond(it.citizen)
}
get<CurrentCitizenRequest> {
if (citizenOrNull === null) {
call.respond(HttpStatusCode.Unauthorized)
} else {
assertCan(VIEW, citizen)
call.respond(citizen)
}
}
put<ChangePasswordCitizenRequest> {
assertCan(CHANGE_PASSWORD, it.citizen)
try {
val content = call.receive<ChangePasswordCitizenRequest.Content>()
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

@@ -3,7 +3,7 @@ package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.Citizen
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.repository.CommentArticle.Sort
import fr.dcproject.security.voter.CommentVoter.Action.CREATE

View File

@@ -1,7 +1,7 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Citizen
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.security.voter.CommentVoter.Action.CREATE

View File

@@ -2,7 +2,7 @@ package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.CitizenWithUserI
import fr.dcproject.component.citizen.CitizenWithUserI
import fr.dcproject.entity.ConstitutionSimple
import fr.dcproject.entity.ConstitutionSimple.TitleSimple
import fr.dcproject.security.voter.ConstitutionVoter.Action.CREATE

View File

@@ -2,7 +2,7 @@ package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.Citizen
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.entity.FollowForUpdate
import fr.dcproject.security.voter.FollowVoter.Action.*
import fr.ktorVoter.assertCan

View File

@@ -1,7 +1,7 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.CitizenRef
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.entity.FollowForUpdate
import fr.dcproject.security.voter.FollowVoter.Action.*

View File

@@ -2,7 +2,7 @@ package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.CitizenRef
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.OpinionChoiceRef
import fr.dcproject.security.voter.OpinionVoter.Action.CREATE
import fr.dcproject.security.voter.OpinionVoter.Action.VIEW
@@ -19,7 +19,7 @@ import io.ktor.util.*
import org.koin.core.KoinComponent
import org.koin.core.get
import java.util.*
import fr.dcproject.entity.Citizen as CitizenEntity
import fr.dcproject.component.citizen.Citizen as CitizenEntity
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
@KtorExperimentalLocationsAPI

View File

@@ -2,7 +2,7 @@ package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.Citizen
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.repository.CommentGeneric
import fr.dcproject.repository.VoteComment

View File

@@ -1,7 +1,7 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.Citizen
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
import fr.dcproject.security.voter.VoteVoter.Action.CREATE

View File

@@ -1,7 +1,7 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.CitizenRef
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.entity.WorkgroupSimple
import fr.dcproject.entity.WorkgroupWithMembersI.Member
import fr.dcproject.entity.WorkgroupWithMembersI.Member.Role

View File

@@ -1,6 +1,6 @@
package fr.dcproject.views
import fr.dcproject.entity.CitizenI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.ViewAggregation
import org.elasticsearch.client.Response
import org.joda.time.DateTime

View File

@@ -1,61 +0,0 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.CitizenBasicI
import fr.dcproject.entity.CitizenWithUserI
import fr.dcproject.user
import fr.dcproject.voter.NoRuleDefinedException
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import io.ktor.application.*
import io.ktor.locations.*
@KtorExperimentalLocationsAPI
class CitizenVoter : Voter<ApplicationCall> {
enum class Action : ActionI {
CREATE,
UPDATE,
VIEW,
DELETE,
CHANGE_PASSWORD
}
override fun invoke(action: Any, context: ApplicationCall, subject: Any?): VoterResponseI {
if (!((action is Action)
&& (subject is CitizenBasicI?))) return abstain()
val user = context.user
if (action == Action.CREATE && user != null) {
return granted()
}
if (action == Action.VIEW) {
if (user == null) return denied("You must be connected to view citizen", "citizen.view.connected")
if (subject is CitizenBasicI) {
return if (subject.isDeleted()) denied("You cannot view a deleted citizen", "citizen.view.deleted")
else granted()
}
throw NoRuleDefinedException(action)
}
if (action == Action.DELETE) {
return denied("You can never deleted a citizen", "citizen.delete.never")
}
if (action == Action.UPDATE) {
if (user == null) return denied("You must be connected to update Citizen", "citizen.update.notConnected")
if (subject !is CitizenWithUserI) throw NoSubjectDefinedException(action)
return if (subject.user.id == user.id) granted() else denied("You can only update your citizen", "citizen.update.notYours")
}
if (action == Action.CHANGE_PASSWORD && user != null && subject is CitizenBasicI) {
val userToChange = subject.user
return if (user.id == userToChange.id) {
granted()
} else {
denied("You can only change your password", "citizen.password.notYours")
}
}
throw NoRuleDefinedException(action)
}
}

View File

@@ -1,7 +1,7 @@
package fr.dcproject.security.voter
import fr.dcproject.citizenOrNull
import fr.dcproject.entity.CitizenI
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.FollowI
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*

View File

@@ -1,7 +1,7 @@
import fr.dcproject.Env
import fr.dcproject.component.article.ArticleRefVersioning
import fr.dcproject.component.article.ArticleViewManager
import fr.dcproject.entity.CitizenRef
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.module
import io.ktor.locations.*
import io.ktor.server.testing.*

View File

@@ -3,7 +3,11 @@ package feature
import fr.dcproject.component.article.ArticleForUpdate
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.entity.WorkgroupRef
import fr.dcproject.repository.CommentArticle
import fr.dcproject.utils.toUUID
import io.cucumber.datatable.DataTable
@@ -13,7 +17,6 @@ import org.koin.test.KoinTest
import org.koin.test.get
import java.util.*
import fr.dcproject.entity.User as UserEntity
import fr.dcproject.repository.Citizen as CitizenRepository
class ArticleSteps : En, KoinTest {
init {

View File

@@ -1,7 +1,8 @@
package feature
import fr.dcproject.entity.Citizen
import fr.dcproject.entity.CitizenI
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.User
import io.cucumber.datatable.DataTable
import io.cucumber.java8.En
@@ -9,7 +10,6 @@ import org.joda.time.DateTime
import org.koin.test.KoinTest
import org.koin.test.get
import java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository
class CitizenSteps : En, KoinTest {
init {

View File

@@ -1,7 +1,13 @@
package feature
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.component.citizen.CitizenWithUserI
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.entity.ConstitutionSimple
import fr.dcproject.repository.CommentConstitution
import fr.dcproject.utils.toUUID
import io.cucumber.datatable.DataTable
@@ -11,7 +17,6 @@ import org.koin.test.KoinTest
import org.koin.test.get
import java.util.*
import fr.dcproject.entity.User as UserEntity
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Constitution as ConstitutionRepository
class ConstitutionSteps : En, KoinTest {

View File

@@ -1,13 +1,13 @@
package feature
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.ConstitutionRef
import fr.dcproject.entity.FollowForUpdate
import fr.dcproject.utils.toUUID
import io.cucumber.java8.En
import org.koin.test.KoinTest
import org.koin.test.get
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository

View File

@@ -2,11 +2,11 @@ package feature
import com.auth0.jwt.JWT
import fr.dcproject.JwtConfig
import fr.dcproject.component.citizen.CitizenRepository
import io.cucumber.java8.En
import io.ktor.http.*
import org.koin.test.KoinTest
import org.koin.test.get
import fr.dcproject.repository.Citizen as CitizenRepository
class KtorServerAuthSteps : En, KoinTest {
init {

View File

@@ -2,6 +2,7 @@ package feature
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.OpinionChoice
import fr.dcproject.entity.OpinionForUpdate
import fr.dcproject.utils.toUUID
@@ -10,7 +11,6 @@ import io.cucumber.java8.En
import org.koin.test.KoinTest
import org.koin.test.get
import java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.OpinionArticle as OpinionRepository
import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository

View File

@@ -1,13 +1,13 @@
package feature
import fr.dcproject.component.article.ArticleRepository
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.utils.toUUID
import io.cucumber.java8.En
import org.koin.test.KoinTest
import org.koin.test.get
import java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.VoteArticle as VoteRepository
class VoteSteps : En, KoinTest {

View File

@@ -1,5 +1,9 @@
package feature
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenRef
import fr.dcproject.component.citizen.CitizenRepository
import fr.dcproject.entity.*
import fr.dcproject.entity.WorkgroupWithMembersI.Member
import fr.dcproject.utils.toUUID
@@ -10,7 +14,6 @@ import org.junit.Assert
import org.koin.test.KoinTest
import org.koin.test.get
import java.util.*
import fr.dcproject.repository.Citizen as CitizenRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepository
class WorkgroupSteps : En, KoinTest {

View File

@@ -2,8 +2,8 @@ package fr.dcproject.security.voter
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleVoter
import fr.dcproject.entity.CitizenCart
import fr.dcproject.entity.CitizenI
import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.voter.Vote.DENIED

View File

@@ -1,22 +1,19 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.CitizenBasic
import fr.dcproject.entity.CitizenI
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.component.citizen.CitizenVoter
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.user
import fr.ktorVoter.*
import io.ktor.application.*
import fr.dcproject.voter.Vote.DENIED
import fr.dcproject.voter.Vote.GRANTED
import io.ktor.locations.*
import io.mockk.every
import io.mockk.mockk
import io.mockk.mockkStatic
import org.amshove.kluent.`should be`
import org.joda.time.DateTime
import org.junit.jupiter.api.Tag
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.assertThrows
@KtorExperimentalLocationsAPI
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -57,78 +54,51 @@ class CitizenVoterTest {
}
@Test
fun `support citizen`(): Unit = CitizenVoter().run {
val p = object : ActionI {}
mockk<ApplicationCall> {
every { user } returns tesla.user
}.let {
this(CitizenVoter.Action.VIEW, it, einstein).vote `should be` Vote.GRANTED
this(p, it, einstein).vote `should be` Vote.ABSTAIN
}
fun `can be view the citizen`() {
CitizenVoter()
.canView(subject = einstein, connectedCitizen = tesla)
.vote `should be` GRANTED
}
@Test
fun `can be view the citizen`(): Unit = listOf(CitizenVoter()).run {
mockk<ApplicationCall> {
every { user } returns tesla.user
}.let {
can(CitizenVoter.Action.VIEW, it, einstein) `should be` true
}
fun `can be view the citizen list`() {
CitizenVoter()
.canView(subjects = listOf(tesla, einstein), connectedCitizen = einstein)
.vote `should be` GRANTED
}
@Test
fun `can be view the citizen list`(): Unit = listOf(CitizenVoter()).run {
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
canAll(CitizenVoter.Action.VIEW, it, listOf(einstein, tesla)) `should be` true
}
fun `can not view deleted citizen`() {
CitizenVoter()
.canView(subject = curie, connectedCitizen = tesla)
.vote `should be` DENIED
}
@Test
fun `can not view deleted citizen`(): Unit = listOf(CitizenVoter()).run {
mockk<ApplicationCall> {
every { user } returns tesla.user
}.let {
can(CitizenVoter.Action.VIEW, it, curie) `should be` false
}
fun `can be update itself`() {
CitizenVoter()
.canUpdate(subject = einstein, connectedCitizen = einstein)
.vote `should be` GRANTED
}
@Test
fun `can be update itself`(): Unit = listOf(CitizenVoter()).run {
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
can(CitizenVoter.Action.UPDATE, it, einstein) `should be` true
}
fun `can not be update other citizen`() {
CitizenVoter()
.canUpdate(subject = tesla, connectedCitizen = einstein)
.vote `should be` DENIED
}
@Test
fun `can not be update other citizen`(): Unit = listOf(CitizenVoter()).run {
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
assertThrows<OneOrMoreVoterDeniedException> {
assertCan(CitizenVoter.Action.UPDATE, it, tesla)
}
}
fun `can be change password of itself`() {
CitizenVoter()
.canChangePassword(subject = einstein, connectedCitizen = einstein)
.vote `should be` GRANTED
}
@Test
fun `can be change password of itself`(): Unit = listOf(CitizenVoter()).run {
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
can(CitizenVoter.Action.CHANGE_PASSWORD, it, einstein) `should be` true
}
}
@Test
fun `can not be change password of other citizen`(): Unit = listOf(CitizenVoter()).run {
mockk<ApplicationCall> {
every { user } returns einstein.user
}.let {
can(CitizenVoter.Action.CHANGE_PASSWORD, it, tesla) `should be` false
}
fun `can not be change password of other citizen`() {
CitizenVoter()
.canChangePassword(subject = tesla, connectedCitizen = einstein)
.vote `should be` DENIED
}
}

View File

@@ -3,10 +3,15 @@ package fr.dcproject.security.voter
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.CommentForUpdate
import fr.dcproject.entity.CommentForView
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import fr.ktorVoter.Vote
import fr.postgresjson.connexion.Paginated
import io.ktor.application.*
import io.ktor.locations.*

View File

@@ -2,7 +2,13 @@ package fr.dcproject.security.voter
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.Follow
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote

View File

@@ -1,7 +1,12 @@
package fr.dcproject.security.voter
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.OpinionChoice
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.user
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote

View File

@@ -1,11 +1,16 @@
package fr.dcproject.security.voter
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.Opinion
import fr.dcproject.entity.OpinionChoice
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.user
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.*
import fr.ktorVoter.Vote
import io.ktor.application.*
import io.ktor.locations.*
import io.mockk.every

View File

@@ -3,7 +3,13 @@ package fr.dcproject.security.voter
import fr.dcproject.citizenOrNull
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.component.article.ArticleRef
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.Citizen
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.entity.VoteForUpdate
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.ActionI
import fr.ktorVoter.Vote

View File

@@ -1,7 +1,13 @@
package fr.dcproject.security.voter
import fr.dcproject.component.article.ArticleForView
import fr.dcproject.entity.*
import fr.dcproject.component.citizen.CitizenBasic
import fr.dcproject.component.citizen.CitizenCart
import fr.dcproject.component.citizen.CitizenI
import fr.dcproject.entity.User
import fr.dcproject.entity.UserI
import fr.dcproject.entity.WorkgroupRef
import fr.dcproject.entity.WorkgroupWithMembersI
import fr.dcproject.user
import fr.dcproject.voter.NoSubjectDefinedException
import fr.ktorVoter.ActionI