#29 Implement Workgroup (route, voter, repo, entity)

Create tests for workgroup routes
add CitizenWithUserI
This commit is contained in:
2020-03-13 21:05:09 +01:00
parent dc034f7c51
commit 27232c5ca9
9 changed files with 399 additions and 9 deletions

View File

@@ -45,11 +45,13 @@ import org.slf4j.event.Level
import java.time.Duration
import java.util.*
import java.util.concurrent.CompletionException
import fr.dcproject.entity.Workgroup as WorkgroupEntity
import fr.dcproject.repository.Article as RepositoryArticle
import fr.dcproject.repository.Citizen as RepositoryCitizen
import fr.dcproject.repository.Constitution as RepositoryConstitution
import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository
import fr.dcproject.repository.User as UserRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepository
fun main(args: Array<String>): Unit = io.ktor.server.jetty.EngineMain.main(args)
@@ -97,7 +99,7 @@ fun Application.module(env: Env = PROD) {
decode { values, _ ->
values.singleOrNull()?.let {
ArticleRef(UUID.fromString(it))
} ?: throw NotFoundException("Article $values not found")
} ?: throw NotFoundException("""UUID "$values" is not valid for Article""")
}
}
@@ -105,14 +107,14 @@ fun Application.module(env: Env = PROD) {
decode { values, _ ->
values.singleOrNull()?.let {
CommentRef(UUID.fromString(it))
} ?: throw NotFoundException("Comment $values not found")
} ?: throw NotFoundException("""UUID "$values" is not valid for Comment""")
}
}
convert<ConstitutionRef> {
decode { values, _ ->
values.singleOrNull()?.let {
ConstitutionRef(UUID.fromString(it))
} ?: throw NotFoundException("Constitution $values not found")
} ?: throw NotFoundException("""UUID "$values" is not valid for Constitution""")
}
}
@@ -136,7 +138,7 @@ fun Application.module(env: Env = PROD) {
decode { values, _ ->
values.singleOrNull()?.let {
CitizenRef(UUID.fromString(it))
} ?: throw NotFoundException("Citizen $values not found")
} ?: throw NotFoundException("""UUID "$values" is not valid for Citizen""")
}
}
@@ -148,6 +150,23 @@ fun Application.module(env: Env = PROD) {
?: throw NotFoundException("OpinionChoice $values not found")
}
}
convert<CitizenRef> {
decode { values, _ ->
values.singleOrNull()?.let {
CitizenRef(UUID.fromString(it))
} ?: throw NotFoundException("""UUID "$values" is not valid for Workgroup""")
}
}
convert<WorkgroupEntity> {
decode { values, _ ->
val id = values.singleOrNull()?.let { UUID.fromString(it) }
?: throw InternalError("Cannot convert $values to UUID")
get<WorkgroupRepository>().findById(id)
?: throw NotFoundException("Workgroup $values not found")
}
}
}
install(Locations) {
@@ -162,7 +181,8 @@ fun Application.module(env: Env = PROD) {
VoteVoter(),
FollowVoter(),
OpinionVoter(),
OpinionChoiceVoter()
OpinionChoiceVoter(),
WorkgroupVoter()
)
}
@@ -299,6 +319,7 @@ fun Application.module(env: Env = PROD) {
voteConstitution(get())
opinionArticle(get())
opinionChoice(get())
workgroup(get())
definition()
}

View File

@@ -37,6 +37,7 @@ import fr.dcproject.repository.User as UserRepository
import fr.dcproject.repository.VoteArticle as VoteArticleRepository
import fr.dcproject.repository.VoteComment as VoteCommentRepository
import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository
import fr.dcproject.repository.Workgroup as WorkgroupRepository
val config = Config()
@@ -111,6 +112,7 @@ val Module = module {
single { VoteCommentRepository(get()) }
single { OpinionChoiceRepository(get()) }
single { OpinionArticleRepository(get()) }
single { WorkgroupRepository(get()) }
// Elasticsearch Client
single<RestClient> {

View File

@@ -41,8 +41,9 @@ open class CitizenSimple(
open class CitizenRefWithUser(
id: UUID = UUID.randomUUID(),
open val user: UserRef
) : CitizenRef(id),
override val user: UserRef
) : CitizenWithUserI,
CitizenRef(id),
EntityDeletedAt by EntityDeletedAtImp()
open class CitizenRef(
@@ -58,15 +59,18 @@ interface CitizenI : UuidEntityI {
)
}
interface CitizenBasicI : CitizenI, EntityDeletedAt {
interface CitizenBasicI : CitizenWithUserI, EntityDeletedAt {
var name: Name
var email: String
var birthday: DateTime
var voteAnonymous: Boolean
var followAnonymous: Boolean
val user: UserI
}
interface CitizenFull : CitizenBasicI {
override val user: User
}
interface CitizenWithUserI : CitizenI {
val user: UserI
}

View File

@@ -0,0 +1,58 @@
package fr.dcproject.entity
import fr.postgresjson.entity.immutable.*
import fr.postgresjson.entity.mutable.EntityDeletedAt
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
import java.util.*
class Workgroup(
id: UUID?,
name: String,
description: String,
logo: String? = null,
anonymous: Boolean = true,
owner: CitizenBasic,
createdBy: CitizenBasic,
override var members: List<CitizenBasic> = emptyList()
) : WorkgroupWithAuthI<CitizenBasic>,
WorkgroupSimple<CitizenBasic>(
id,
name,
description,
logo,
anonymous,
owner,
createdBy
),
EntityCreatedAt by EntityCreatedAtImp(),
EntityUpdatedAt by EntityUpdatedAtImp()
open class WorkgroupSimple<Z : CitizenRef>(
id: UUID?,
var name: String,
var description: String,
var logo: String? = null,
var anonymous: Boolean = true,
var owner: Z,
createdBy: Z
) : WorkgroupRef(id),
EntityCreatedBy<Z> by EntityCreatedByImp(createdBy),
EntityDeletedAt by EntityDeletedAtImp()
open class WorkgroupRef(
id: UUID?
) : UuidEntity(id ?: UUID.randomUUID()), WorkgroupI
interface WorkgroupWithAuthI<Z : CitizenWithUserI> : WorkgroupI, EntityCreatedBy<Z>, EntityDeletedAt {
val anonymous: Boolean
val owner: Z
var members: List<Z>
fun isMember(user: UserI): Boolean =
members.map { it.user.id }.contains(user.id) || owner.user.id == user.id
fun isMember(citizen: CitizenWithUserI): Boolean =
isMember(citizen.user)
}
interface WorkgroupI : UuidEntityI

View File

@@ -0,0 +1,46 @@
package fr.dcproject.repository
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.WorkgroupSimple
import fr.postgresjson.connexion.Paginated
import fr.postgresjson.connexion.Requester
import fr.postgresjson.entity.Parameter
import fr.postgresjson.repository.RepositoryI
import fr.postgresjson.repository.RepositoryI.Direction
import net.pearx.kasechange.toSnakeCase
import java.util.*
import fr.dcproject.entity.Workgroup as WorkgroupEntity
class Workgroup(override var requester: Requester) : RepositoryI {
fun findById(id: UUID): WorkgroupEntity? {
val function = requester.getFunction("find_workgroup_by_id")
return function.selectOne("id" to id)
}
fun find(
page: Int = 1,
limit: Int = 50,
sort: String? = null,
direction: Direction? = null,
search: String? = null,
filter: Filter = Filter()
): Paginated<WorkgroupEntity> {
return requester
.getFunction("find_workgroups")
.select(
page, limit,
"sort" to sort?.toSnakeCase(),
"direction" to direction,
"search" to search,
"filter" to filter
)
}
fun upsert(workgroup: WorkgroupSimple<CitizenRef>): WorkgroupEntity = requester
.getFunction("upsert_workgroup")
.selectOne("resource" to workgroup) ?: error("query 'upsert_workgroup' return null")
class Filter(
val createdById: String? = null
) : Parameter
}

View File

@@ -0,0 +1,99 @@
package fr.dcproject.routes
import fr.dcproject.citizen
import fr.dcproject.entity.CitizenRef
import fr.dcproject.entity.WorkgroupSimple
import fr.dcproject.entity.request.RequestBuilder
import fr.dcproject.entity.request.getContent
import fr.dcproject.repository.Workgroup.Filter
import fr.dcproject.security.voter.WorkgroupVoter.Action.VIEW
import fr.dcproject.security.voter.WorkgroupVoter.Action.CREATE
import fr.dcproject.security.voter.assertCan
import fr.postgresjson.repository.RepositoryI
import io.ktor.application.ApplicationCall
import io.ktor.application.call
import io.ktor.http.HttpStatusCode
import io.ktor.locations.KtorExperimentalLocationsAPI
import io.ktor.locations.Location
import io.ktor.locations.get
import io.ktor.locations.post
import io.ktor.request.receive
import io.ktor.response.respond
import io.ktor.routing.Route
import org.koin.core.KoinComponent
import java.util.*
import fr.dcproject.entity.Workgroup as WorkgroupEntity
import fr.dcproject.repository.Workgroup as WorkgroupRepository
@KtorExperimentalLocationsAPI
object WorkgroupsPaths {
@Location("/workgroups")
class WorkgroupsRequest(
page: Int = 1,
limit: Int = 50,
val sort: String? = null,
val direction: RepositoryI.Direction? = null,
val search: String? = null,
val createdBy: 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("/workgroups/{workgroup}")
class WorkgroupRequest(val workgroup: WorkgroupEntity)
@Location("/workgroups")
class PostWorkgroupRequest : RequestBuilder<WorkgroupSimple<CitizenRef>> {
class Content(
val id: UUID?,
val name: String,
val description: String,
val logo: String?,
val anonymous: Boolean?,
val owner: CitizenRef?
) : KoinComponent {
fun create(creator: CitizenRef): WorkgroupSimple<CitizenRef> {
return WorkgroupSimple(
id ?: UUID.randomUUID(),
name,
description,
logo,
anonymous ?: true,
owner ?: creator,
creator
)
}
}
override suspend fun getContent(call: ApplicationCall): WorkgroupSimple<CitizenRef> {
return call.receive<Content>().create(call.citizen)
}
}
}
@KtorExperimentalLocationsAPI
fun Route.workgroup(repo: WorkgroupRepository) {
get<WorkgroupsPaths.WorkgroupsRequest> {
val workgroups =
repo.find(it.page, it.limit, it.sort, it.direction, it.search, Filter(createdById = it.createdBy))
assertCan(VIEW, workgroups.result)
call.respond(workgroups)
}
get<WorkgroupsPaths.WorkgroupRequest> {
assertCan(VIEW, it.workgroup)
call.respond(it.workgroup)
}
post<WorkgroupsPaths.PostWorkgroupRequest> {
call.getContent(it)
.let { workgroup ->
assertCan(CREATE, workgroup)
repo.upsert(workgroup)
}.let {
call.respond(HttpStatusCode.Created, it)
}
}
}

View File

@@ -0,0 +1,63 @@
package fr.dcproject.security.voter
import fr.dcproject.entity.UserI
import fr.dcproject.entity.WorkgroupI
import fr.dcproject.entity.WorkgroupWithAuthI
import io.ktor.application.ApplicationCall
class WorkgroupVoter : Voter {
enum class Action : ActionI {
CREATE,
UPDATE,
VIEW,
DELETE
}
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
return (action is Action)
.and(subject is List<*> || subject is WorkgroupI?)
}
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
val user = call.user
if (subject is WorkgroupI && action == Action.CREATE && user is UserI) {
return Vote.GRANTED
}
if (action == Action.VIEW) {
if (subject is WorkgroupWithAuthI<*>) {
return if (subject.isDeleted()) Vote.DENIED
else if (!subject.anonymous) Vote.GRANTED
else if (subject.anonymous && user != null && subject.isMember(user)) Vote.GRANTED
else Vote.DENIED
}
if (subject is List<*>) {
subject.forEach {
if (it !is WorkgroupWithAuthI<*> || it.isDeleted()) {
return Vote.DENIED
}
}
return Vote.GRANTED
}
return Vote.DENIED
}
if (subject is WorkgroupWithAuthI<*>) {
if (action == Action.DELETE && user is UserI && subject.owner.user.id == user.id) {
return Vote.GRANTED
}
if (action == Action.UPDATE && user is UserI && subject.owner.user.id == user.id) {
return Vote.GRANTED
}
return Vote.DENIED
}
if (action is Action) {
return Vote.DENIED
}
return Vote.ABSTAIN
}
}