Remove sub directories
This commit is contained in:
@@ -1,364 +0,0 @@
|
||||
package fr.dcproject
|
||||
|
||||
import com.fasterxml.jackson.core.util.DefaultIndenter
|
||||
import com.fasterxml.jackson.core.util.DefaultPrettyPrinter
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
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 fr.dcproject.Env.PROD
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.event.EventSubscriber
|
||||
import fr.dcproject.event.configEvent
|
||||
import fr.dcproject.routes.*
|
||||
import fr.dcproject.security.voter.*
|
||||
import fr.ktorVoter.AuthorizationVoter
|
||||
import fr.ktorVoter.ForbiddenException
|
||||
import fr.postgresjson.migration.Migrations
|
||||
import io.ktor.application.Application
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.call
|
||||
import io.ktor.application.install
|
||||
import io.ktor.auth.Authentication
|
||||
import io.ktor.auth.authenticate
|
||||
import io.ktor.auth.jwt.jwt
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.engine.jetty.Jetty
|
||||
import io.ktor.features.*
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpMethod
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.http.auth.HttpAuthHeader
|
||||
import io.ktor.jackson.jackson
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Locations
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Routing
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import io.ktor.websocket.WebSockets
|
||||
import org.eclipse.jetty.util.log.Slf4jLog
|
||||
import org.elasticsearch.client.Request
|
||||
import org.elasticsearch.client.RestClient
|
||||
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.*
|
||||
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)
|
||||
|
||||
enum class Env { PROD, TEST, CUCUMBER }
|
||||
|
||||
@KtorExperimentalAPI
|
||||
@KtorExperimentalLocationsAPI
|
||||
@Suppress("unused") // Referenced in application.conf
|
||||
fun Application.module(env: Env = PROD) {
|
||||
install(Koin) {
|
||||
Slf4jLog()
|
||||
modules(Module)
|
||||
}
|
||||
|
||||
install(CallLogging) {
|
||||
level = Level.INFO
|
||||
}
|
||||
|
||||
install(DataConversion) {
|
||||
convert<UUID> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
}
|
||||
|
||||
encode { value ->
|
||||
when (value) {
|
||||
null -> listOf()
|
||||
is UUID -> listOf(value.toString())
|
||||
else -> throw InternalError("Cannot convert $value as UUID")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: create generic convert for entityI
|
||||
convert<Article> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
get<RepositoryArticle>().findById(UUID.fromString(it))
|
||||
?: throw NotFoundException("Article $values not found")
|
||||
} ?: throw NotFoundException("Article $values not found")
|
||||
}
|
||||
}
|
||||
convert<ArticleRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
ArticleRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Article""")
|
||||
}
|
||||
}
|
||||
|
||||
convert<CommentRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
CommentRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Comment""")
|
||||
}
|
||||
}
|
||||
convert<ConstitutionRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
ConstitutionRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Constitution""")
|
||||
}
|
||||
}
|
||||
|
||||
convert<Constitution> {
|
||||
decode { values, _ ->
|
||||
val id = values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
?: throw InternalError("Cannot convert $values to UUID")
|
||||
get<RepositoryConstitution>().findById(id) ?: throw NotFoundException("Constitution $values not found")
|
||||
}
|
||||
}
|
||||
|
||||
convert<Citizen> {
|
||||
decode { values, _ ->
|
||||
val id = values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
?: throw InternalError("Cannot convert $values to UUID")
|
||||
get<RepositoryCitizen>().findById(id, true) ?: throw NotFoundException("Citizen $values not found")
|
||||
}
|
||||
}
|
||||
|
||||
convert<CitizenRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
CitizenRef(UUID.fromString(it))
|
||||
} ?: throw NotFoundException("""UUID "$values" is not valid for Citizen""")
|
||||
}
|
||||
}
|
||||
|
||||
convert<OpinionChoice> {
|
||||
decode { values, _ ->
|
||||
val id = values.singleOrNull()?.let { UUID.fromString(it) }
|
||||
?: throw InternalError("Cannot convert $values to UUID")
|
||||
get<OpinionChoiceRepository>().findOpinionChoiceById(id)
|
||||
?: throw NotFoundException("OpinionChoice $values not found")
|
||||
}
|
||||
}
|
||||
|
||||
convert<WorkgroupRef> {
|
||||
decode { values, _ ->
|
||||
values.singleOrNull()?.let {
|
||||
WorkgroupRef(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) {
|
||||
}
|
||||
|
||||
install(AuthorizationVoter) {
|
||||
voters = mutableListOf(
|
||||
ArticleVoter(),
|
||||
ConstitutionVoter(),
|
||||
CitizenVoter(),
|
||||
CommentVoter(),
|
||||
VoteVoter(),
|
||||
FollowVoter(),
|
||||
OpinionVoter(),
|
||||
OpinionChoiceVoter(),
|
||||
WorkgroupVoter()
|
||||
)
|
||||
}
|
||||
|
||||
HttpClient(Jetty) {
|
||||
engine {
|
||||
}
|
||||
}
|
||||
|
||||
/* Create index if not exist */
|
||||
get<RestClient>().run {
|
||||
if (performRequest(Request("HEAD", "/views?include_type_name=false")).statusLine.statusCode == 404) {
|
||||
Request(
|
||||
"PUT",
|
||||
"/views?include_type_name=false"
|
||||
).apply {
|
||||
//language=JSON
|
||||
setJsonEntity(
|
||||
"""
|
||||
{
|
||||
"settings": {
|
||||
"number_of_shards": 5
|
||||
},
|
||||
"mappings": {
|
||||
"properties": {
|
||||
"logged": {
|
||||
"type": "boolean"
|
||||
},
|
||||
"type": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"user_ref": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"version_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"ip": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"citizen_id": {
|
||||
"type": "keyword"
|
||||
},
|
||||
"view_at": {
|
||||
"type": "date"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
)
|
||||
}.let {
|
||||
performRequest(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
install(WebSockets) {
|
||||
pingPeriod = Duration.ofSeconds(60) // Disabled (null) by default
|
||||
timeout = Duration.ofSeconds(15)
|
||||
maxFrameSize = Long.MAX_VALUE // Disabled (max value). The connection will be closed if surpassed this length.
|
||||
masking = false
|
||||
}
|
||||
|
||||
install(EventSubscriber) {
|
||||
configEvent(get(), get(), get(), get())
|
||||
}
|
||||
|
||||
install(Authentication) {
|
||||
/**
|
||||
* Setup the JWT authentication to be used in [Routing].
|
||||
* If the token is valid, the corresponding [User] is fetched from the database.
|
||||
* The [User] can then be accessed in each [ApplicationCall].
|
||||
*/
|
||||
jwt {
|
||||
verifier(JwtConfig.verifier)
|
||||
realm = "dc-project.fr"
|
||||
validate {
|
||||
it.payload.getClaim("id").asString()?.let { id ->
|
||||
get<UserRepository>().findById(UUID.fromString(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
jwt("url") {
|
||||
verifier(JwtConfig.verifier)
|
||||
realm = "dc-project.fr"
|
||||
authHeader { call ->
|
||||
call.request.queryParameters.get("token")?.let {
|
||||
HttpAuthHeader.Single("Bearer", it)
|
||||
}
|
||||
}
|
||||
validate {
|
||||
it.payload.getClaim("id").asString()?.let { id ->
|
||||
get<UserRepository>().findById(UUID.fromString(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
install(AutoHeadResponse)
|
||||
|
||||
install(ContentNegotiation) {
|
||||
jackson {
|
||||
propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
|
||||
|
||||
registerModule(JodaModule())
|
||||
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false)
|
||||
configure(SerializationFeature.INDENT_OUTPUT, true)
|
||||
setDefaultPrettyPrinter(DefaultPrettyPrinter().apply {
|
||||
indentArraysWith(DefaultPrettyPrinter.FixedSpaceIndenter.instance)
|
||||
indentObjectsWith(DefaultIndenter(" ", "\n"))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
install(Routing) {
|
||||
// trace { application.log.trace(it.buildText()) }
|
||||
authenticate(optional = true) {
|
||||
article(get(), get())
|
||||
auth(get(), get(), get())
|
||||
citizen(get(), get())
|
||||
constitution(get())
|
||||
followArticle(get())
|
||||
followConstitution(get())
|
||||
comment(get())
|
||||
commentArticle(get())
|
||||
commentConstitution(get())
|
||||
voteArticle(get(), get(), get())
|
||||
voteConstitution(get())
|
||||
opinionArticle(get())
|
||||
opinionChoice(get())
|
||||
workgroup(get())
|
||||
definition()
|
||||
}
|
||||
|
||||
authenticate("url") {
|
||||
notificationArticle(get(), get(named("ws")))
|
||||
}
|
||||
}
|
||||
|
||||
install(StatusPages) {
|
||||
// TODO move to postgresJson lib
|
||||
exception<CompletionException> { e ->
|
||||
val parent = e.cause?.cause
|
||||
if (parent is GenericDatabaseException) {
|
||||
call.respond(HttpStatusCode.BadRequest, parent.errorMessage.message!!)
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
}
|
||||
exception<NotFoundException> { e ->
|
||||
call.respond(HttpStatusCode.NotFound, e.message!!)
|
||||
}
|
||||
exception<ForbiddenException> {
|
||||
call.respond(HttpStatusCode.Forbidden)
|
||||
}
|
||||
}
|
||||
|
||||
install(CORS) {
|
||||
method(HttpMethod.Options)
|
||||
method(HttpMethod.Put)
|
||||
method(HttpMethod.Delete)
|
||||
header(HttpHeaders.Authorization)
|
||||
anyHost()
|
||||
// host("localhost:4200", schemes = listOf("http", "https"))
|
||||
allowCredentials = true
|
||||
allowSameOrigin = true
|
||||
maxAge = Duration.ofDays(1)
|
||||
}
|
||||
|
||||
if (env == PROD) {
|
||||
get<Migrations>().run()
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package fr.dcproject
|
||||
|
||||
import fr.dcproject.entity.User
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.ktorVoter.ForbiddenException
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.auth.authentication
|
||||
import io.ktor.util.AttributeKey
|
||||
import io.ktor.util.pipeline.PipelineContext
|
||||
import org.koin.core.context.GlobalContext
|
||||
import fr.dcproject.entity.Citizen as CitizenEntity
|
||||
import fr.dcproject.repository.Citizen as CitizenRepository
|
||||
|
||||
private val citizenAttributeKey = AttributeKey<CitizenEntity>("CitizenContext")
|
||||
|
||||
val ApplicationCall.citizen: CitizenEntity
|
||||
get() = attributes.computeIfAbsent(citizenAttributeKey) {
|
||||
val user = authentication.principal<UserI>() ?: throw ForbiddenException()
|
||||
GlobalContext.get().koin.get<CitizenRepository>().findByUser(user)
|
||||
?: throw ForbiddenException("Citizen not found for this user id \"${user.id}\"")
|
||||
}
|
||||
|
||||
val ApplicationCall.citizenOrNull: CitizenEntity?
|
||||
get() = authentication.principal<UserI>()?.let {
|
||||
GlobalContext.get().koin.get<CitizenRepository>().findByUser(it)
|
||||
}
|
||||
|
||||
val PipelineContext<Unit, ApplicationCall>.citizen get() = context.citizen
|
||||
val PipelineContext<Unit, ApplicationCall>.citizenOrNull get() = context.citizenOrNull
|
||||
|
||||
val ApplicationCall.user get() = authentication.principal<User>()
|
||||
@@ -1,62 +0,0 @@
|
||||
package fr.dcproject
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.JWTVerifier
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import com.typesafe.config.ConfigFactory
|
||||
import fr.dcproject.entity.UserI
|
||||
import java.util.*
|
||||
import java.net.URI
|
||||
|
||||
object Config {
|
||||
private var config = ConfigFactory.load()
|
||||
|
||||
object Sql {
|
||||
val migrationFiles: URI = this::class.java.getResource("/sql/migrations").toURI()
|
||||
val functionFiles: URI = this::class.java.getResource("/sql/functions").toURI()
|
||||
val fixtureFiles: URI = this::class.java.getResource("/sql/fixtures").toURI()
|
||||
}
|
||||
|
||||
val envName: String = config.getString("app.envName")
|
||||
val domain: String = config.getString("app.domain")
|
||||
|
||||
val host: String = config.getString("db.host")
|
||||
var database: String = config.getString("db.database")
|
||||
var username: String = config.getString("db.username")
|
||||
var password: String = config.getString("db.password")
|
||||
val port: Int = config.getInt("db.port")
|
||||
val redis: String = config.getString("redis.connection")
|
||||
val elasticsearch: String = config.getString("elasticsearch.connection")
|
||||
val rabbitmq: String = config.getString("rabbitmq.connection")
|
||||
val exchangeNotificationName = "notification"
|
||||
val sendGridKey: String = config.getString("mail.sendGrid.key")
|
||||
}
|
||||
|
||||
object JwtConfig {
|
||||
private const val secret = "zAP5MBA4B4Ijz0MZaS48"
|
||||
const val issuer = "dc-project.fr"
|
||||
private const val validityInMs = 3_600_000 * 10 // 10 hours
|
||||
|
||||
// TODO change to RSA512
|
||||
val algorithm = Algorithm.HMAC512(secret)
|
||||
|
||||
val verifier: JWTVerifier = JWT
|
||||
.require(algorithm)
|
||||
.withIssuer(issuer)
|
||||
.build()
|
||||
|
||||
/**
|
||||
* Produce a token for this combination of User and Account
|
||||
*/
|
||||
fun makeToken(user: UserI): String = JWT.create()
|
||||
.withSubject("Authentication")
|
||||
.withIssuer(issuer)
|
||||
.withClaim("id", user.id.toString())
|
||||
.withExpiresAt(getExpiration())
|
||||
.sign(algorithm)
|
||||
|
||||
/**
|
||||
* Calculate the expiration Date based on current time + the given validity
|
||||
*/
|
||||
private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
|
||||
}
|
||||
@@ -1,129 +0,0 @@
|
||||
package fr.dcproject
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.fasterxml.jackson.databind.PropertyNamingStrategy
|
||||
import com.fasterxml.jackson.databind.SerializationFeature
|
||||
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||
import com.fasterxml.jackson.datatype.joda.JodaModule
|
||||
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
|
||||
import com.rabbitmq.client.ConnectionFactory
|
||||
import fr.dcproject.messages.Mailer
|
||||
import fr.dcproject.messages.SsoManager
|
||||
import fr.dcproject.views.ArticleViewManager
|
||||
import fr.postgresjson.connexion.Connection
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.migration.Migrations
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.client.features.websocket.WebSockets
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import io.lettuce.core.RedisClient
|
||||
import io.lettuce.core.api.async.RedisAsyncCommands
|
||||
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.Article as ArticleRepository
|
||||
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
|
||||
import fr.dcproject.repository.Constitution as ConstitutionRepository
|
||||
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
|
||||
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository
|
||||
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
|
||||
import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository
|
||||
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
|
||||
|
||||
@KtorExperimentalAPI
|
||||
val Module = module {
|
||||
|
||||
single { Config }
|
||||
|
||||
// SQL connection
|
||||
single {
|
||||
Connection(
|
||||
host = Config.host,
|
||||
port = Config.port,
|
||||
database = Config.database,
|
||||
username = Config.username,
|
||||
password = Config.password
|
||||
)
|
||||
}
|
||||
|
||||
// Launch Database migration
|
||||
single { Migrations(get(), Config.Sql.migrationFiles, Config.Sql.functionFiles) }
|
||||
|
||||
// Redis client
|
||||
single<RedisAsyncCommands<String, String>> {
|
||||
RedisClient.create(Config.redis).connect()?.async() ?: error("Unable to connect to redis")
|
||||
}
|
||||
|
||||
// RabbitMQ
|
||||
single<ConnectionFactory> {
|
||||
ConnectionFactory().apply { setUri(Config.rabbitmq) }
|
||||
}
|
||||
|
||||
// JsonSerializer
|
||||
single<ObjectMapper> {
|
||||
jacksonObjectMapper().apply {
|
||||
registerModule(SimpleModule())
|
||||
propertyNamingStrategy = PropertyNamingStrategy.SNAKE_CASE
|
||||
|
||||
registerModule(JodaModule())
|
||||
disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
|
||||
configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Client HTTP for WebSockets
|
||||
single(named("ws")) {
|
||||
HttpClient {
|
||||
install(WebSockets)
|
||||
}
|
||||
}
|
||||
|
||||
// SQL Requester (postgresJson)
|
||||
single {
|
||||
Requester.RequesterFactory(
|
||||
connection = get(),
|
||||
functionsDirectory = Config.Sql.functionFiles
|
||||
).createRequester()
|
||||
}
|
||||
|
||||
// Repositories
|
||||
single { UserRepository(get()) }
|
||||
single { ArticleRepository(get()) }
|
||||
single { CitizenRepository(get()) }
|
||||
single { ConstitutionRepository(get()) }
|
||||
single { FollowArticleRepository(get()) }
|
||||
single { FollowConstitutionRepository(get()) }
|
||||
single { CommentGenericRepository(get()) }
|
||||
single { CommentArticleRepository(get()) }
|
||||
single { CommentConstitutionRepository(get()) }
|
||||
single { VoteArticleRepository(get()) }
|
||||
single { VoteConstitutionRepository(get()) }
|
||||
single { VoteCommentRepository(get()) }
|
||||
single { OpinionChoiceRepository(get()) }
|
||||
single { OpinionArticleRepository(get()) }
|
||||
single { WorkgroupRepository(get()) }
|
||||
|
||||
// Elasticsearch Client
|
||||
single<RestClient> {
|
||||
RestClient.builder(
|
||||
HttpHost.create(Config.elasticsearch)
|
||||
).build()
|
||||
}
|
||||
|
||||
single { ArticleViewManager(get()) }
|
||||
|
||||
// Mailler
|
||||
single { Mailer(Config.sendGridKey) }
|
||||
|
||||
// SSO Manager for connection
|
||||
single { SsoManager(get<Mailer>(), Config.domain, get()) }
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import fr.postgresjson.entity.mutable.EntityVersioning
|
||||
import fr.postgresjson.entity.mutable.UuidEntityVersioning
|
||||
import java.util.*
|
||||
|
||||
class Article(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
title: String,
|
||||
override var anonymous: Boolean = true,
|
||||
override var content: String,
|
||||
override var description: String,
|
||||
override var tags: List<String> = emptyList(),
|
||||
draft: Boolean = false,
|
||||
override var lastVersion: Boolean = false,
|
||||
override val createdBy: CitizenBasic
|
||||
) : ArticleFull,
|
||||
ArticleAuthI<CitizenBasicI>,
|
||||
ArticleSimple(id, title, createdBy, draft),
|
||||
Viewable by ViewableImp() {
|
||||
init {
|
||||
tags = tags.distinct()
|
||||
}
|
||||
}
|
||||
|
||||
open class ArticleSimple(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override var title: String,
|
||||
override val createdBy: CitizenBasic,
|
||||
override var draft: Boolean = false
|
||||
) : ArticleSimpleI,
|
||||
ArticleAuthI<CitizenBasicI>,
|
||||
ArticleRefVersioning(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
|
||||
EntityDeletedAt by EntityDeletedAtImp(),
|
||||
Votable by VotableImp(),
|
||||
Opinionable by OpinionableImp()
|
||||
|
||||
open class ArticleRefVersioning(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
versionNumber: Int? = null,
|
||||
versionId: UUID = UUID.randomUUID()
|
||||
) : ArticleRef(id),
|
||||
EntityVersioning<UUID, Int> by UuidEntityVersioning(versionNumber, versionId)
|
||||
|
||||
open class ArticleRef(
|
||||
id: UUID = UUID.randomUUID()
|
||||
) : ArticleI, TargetRef(id)
|
||||
|
||||
interface ArticleI : UuidEntityI, TargetI
|
||||
|
||||
interface ArticleSimpleI :
|
||||
ArticleI,
|
||||
EntityVersioning<UUID, Int>,
|
||||
EntityCreatedBy<CitizenBasicI>,
|
||||
EntityCreatedAt,
|
||||
EntityDeletedAt,
|
||||
Votable {
|
||||
var title: String
|
||||
}
|
||||
|
||||
interface ArticleBasicI :
|
||||
ArticleSimpleI {
|
||||
var anonymous: Boolean
|
||||
var content: String
|
||||
var description: String
|
||||
var tags: List<String>
|
||||
}
|
||||
|
||||
interface ArticleFull :
|
||||
ArticleBasicI {
|
||||
var draft: Boolean
|
||||
var lastVersion: Boolean
|
||||
}
|
||||
|
||||
interface ArticleAuthI<U : CitizenWithUserI> :
|
||||
ArticleI,
|
||||
EntityCreatedBy<U>,
|
||||
EntityDeletedAt {
|
||||
var draft: Boolean
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.dcproject.entity.CitizenI.Name
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.immutable.UuidEntityI
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
class Citizen(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
name: Name,
|
||||
email: String,
|
||||
birthday: DateTime,
|
||||
voteAnonymous: Boolean = true,
|
||||
followAnonymous: Boolean = true,
|
||||
override val user: User
|
||||
) : CitizenFull,
|
||||
CitizenBasic(id, name, email, birthday, voteAnonymous, followAnonymous, user),
|
||||
EntityCreatedAt by EntityCreatedAtImp() {
|
||||
var workgroups: List<WorkgroupSimple<CitizenRef>> = emptyList()
|
||||
}
|
||||
|
||||
open class CitizenBasic(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
name: Name,
|
||||
override var email: String,
|
||||
override var birthday: DateTime,
|
||||
override var voteAnonymous: Boolean = true,
|
||||
override var followAnonymous: Boolean = true,
|
||||
override val user: User
|
||||
) : CitizenBasicI,
|
||||
CitizenSimple(id, name, user)
|
||||
|
||||
open class CitizenSimple(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
var name: Name,
|
||||
user: UserRef
|
||||
) : CitizenRefWithUser(id, user)
|
||||
|
||||
open class CitizenRefWithUser(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val user: UserRef
|
||||
) : CitizenWithUserI,
|
||||
CitizenRef(id),
|
||||
EntityDeletedAt by EntityDeletedAtImp()
|
||||
|
||||
open class CitizenRef(
|
||||
id: UUID = UUID.randomUUID()
|
||||
) : UuidEntity(id),
|
||||
CitizenI
|
||||
|
||||
interface CitizenI : UuidEntityI {
|
||||
data class Name(
|
||||
var firstName: String,
|
||||
var lastName: String,
|
||||
var civility: String? = null
|
||||
)
|
||||
}
|
||||
|
||||
interface CitizenBasicI : CitizenWithUserI, EntityDeletedAt {
|
||||
var name: Name
|
||||
var email: String
|
||||
var birthday: DateTime
|
||||
var voteAnonymous: Boolean
|
||||
var followAnonymous: Boolean
|
||||
}
|
||||
|
||||
interface CitizenFull : CitizenBasicI {
|
||||
override val user: User
|
||||
}
|
||||
|
||||
interface CitizenWithUserI : CitizenI {
|
||||
val user: UserI
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import java.util.*
|
||||
|
||||
open class Comment<T : TargetI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: CitizenBasic,
|
||||
override var target: T,
|
||||
var content: String,
|
||||
val responses: List<Comment<T>>? = null,
|
||||
var parent: Comment<T>? = null,
|
||||
val parentsIds: List<UUID>? = null,
|
||||
val childrenCount: Int? = null
|
||||
) : ExtraI<T, CitizenBasicI>,
|
||||
CommentRef(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
|
||||
EntityUpdatedAt by EntityUpdatedAtImp(),
|
||||
EntityDeletedAt by EntityDeletedAtImp(),
|
||||
Votable by VotableImp(),
|
||||
TargetI {
|
||||
constructor(
|
||||
createdBy: CitizenBasic,
|
||||
parent: Comment<T>,
|
||||
content: String
|
||||
) : this(
|
||||
createdBy = createdBy,
|
||||
parent = parent,
|
||||
target = parent.target,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
open class CommentRef(id: UUID = UUID.randomUUID()) : CommentS(id)
|
||||
|
||||
sealed class CommentS(id: UUID) : TargetRef(id)
|
||||
@@ -1,69 +0,0 @@
|
||||
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 Constitution(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
title: String,
|
||||
anonymous: Boolean = true,
|
||||
titles: MutableList<TitleSimple<ArticleSimple>> = mutableListOf(),
|
||||
draft: Boolean = false,
|
||||
lastVersion: Boolean = false,
|
||||
override val createdBy: CitizenSimple
|
||||
) : ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleSimple>>(
|
||||
id,
|
||||
title = title,
|
||||
anonymous = anonymous,
|
||||
titles = titles,
|
||||
draft = draft,
|
||||
lastVersion = lastVersion,
|
||||
createdBy = createdBy
|
||||
) {
|
||||
|
||||
class Title(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
name: String,
|
||||
rank: Int? = null,
|
||||
override val articles: MutableList<ArticleSimple> = mutableListOf()
|
||||
) : ConstitutionSimple.TitleSimple<ArticleSimple>(id, name, rank)
|
||||
}
|
||||
|
||||
open class ConstitutionSimple<Cr : CitizenRefWithUser, T : ConstitutionSimple.TitleSimple<*>>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
var title: String,
|
||||
var anonymous: Boolean = true,
|
||||
open var titles: MutableList<T> = mutableListOf(),
|
||||
var draft: Boolean = false,
|
||||
var lastVersion: Boolean = false,
|
||||
override val createdBy: Cr,
|
||||
versionId: UUID = UUID.randomUUID()
|
||||
) : ConstitutionRef(id),
|
||||
EntityVersioning<UUID, Int?> by UuidEntityVersioning(versionId = versionId),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<Cr> by EntityCreatedByImp(createdBy),
|
||||
EntityDeletedAt by EntityDeletedAtImp() {
|
||||
|
||||
init {
|
||||
titles.forEachIndexed { index, title ->
|
||||
title.rank = index
|
||||
}
|
||||
}
|
||||
|
||||
open class TitleSimple<A : ArticleI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
var name: String,
|
||||
var rank: Int? = null,
|
||||
open val articles: MutableList<A> = mutableListOf()
|
||||
) : TitleRef(id)
|
||||
}
|
||||
|
||||
open class ConstitutionRef(id: UUID = UUID.randomUUID()) : ConstitutionS(id) {
|
||||
open class TitleRef(
|
||||
id: UUID = UUID.randomUUID()
|
||||
) : UuidEntity(id)
|
||||
}
|
||||
|
||||
sealed class ConstitutionS(id: UUID = UUID.randomUUID()) : TargetRef(id), TargetI
|
||||
@@ -1,58 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedBy
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.immutable.UuidEntityI
|
||||
import java.util.*
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.isSubclassOf
|
||||
|
||||
interface ExtraI<T : TargetI, C : CitizenI> :
|
||||
UuidEntityI,
|
||||
EntityCreatedAt,
|
||||
EntityCreatedBy<C> {
|
||||
val target: T
|
||||
}
|
||||
|
||||
open class TargetRef(id: UUID = UUID.randomUUID(), reference: String = "") : TargetI, UuidEntity(id) {
|
||||
|
||||
final override val reference: String
|
||||
get() = if (field != "") field else TargetI.getReference(this)
|
||||
|
||||
init {
|
||||
this.reference = reference
|
||||
}
|
||||
}
|
||||
|
||||
interface TargetI : UuidEntityI {
|
||||
enum class TargetName(val targetReference: String) {
|
||||
Article("article"),
|
||||
Constitution("constitution"),
|
||||
Comment("comment"),
|
||||
Opinion("opinion")
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun <T : TargetI> getReference(t: KClass<T>): String {
|
||||
return when {
|
||||
t.isSubclassOf(ArticleRef::class) -> TargetName.Article.targetReference
|
||||
t.isSubclassOf(ConstitutionRef::class) -> TargetName.Constitution.targetReference
|
||||
t.isSubclassOf(CommentRef::class) -> TargetName.Comment.targetReference
|
||||
t.isSubclassOf(Opinion::class) -> TargetName.Opinion.targetReference
|
||||
else -> throw error("target not implemented: ${t.qualifiedName} \nImplement it or return 'reference' from SQL")
|
||||
}
|
||||
}
|
||||
|
||||
fun getReference(t: TargetI): String {
|
||||
val ref = this.getReference(t::class)
|
||||
return if (t is ExtraI<*, *>) {
|
||||
"${ref}_on_${t.target.reference}"
|
||||
} else {
|
||||
ref
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val reference: String
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import java.util.*
|
||||
|
||||
class Follow<T : TargetI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: CitizenBasic,
|
||||
override var target: T
|
||||
) : ExtraI<T, CitizenBasicI>,
|
||||
FollowSimple<T, CitizenBasicI>(id, createdBy, target)
|
||||
|
||||
open class FollowSimple<T : TargetI, C : CitizenI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: C,
|
||||
override var target: T
|
||||
) : ExtraI<T, C>,
|
||||
UuidEntity(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<C> by EntityCreatedByImp(createdBy)
|
||||
@@ -1,27 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedBy
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedByImp
|
||||
import java.util.*
|
||||
|
||||
open class Opinion<T : TargetI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: CitizenBasic,
|
||||
override val target: T,
|
||||
val choice: OpinionChoice
|
||||
) : ExtraI<T, CitizenBasicI>,
|
||||
TargetRef(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy) {
|
||||
|
||||
fun getName(): String = choice.name
|
||||
}
|
||||
|
||||
class OpinionArticle(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
createdBy: CitizenBasic,
|
||||
target: ArticleRef,
|
||||
choice: OpinionChoice
|
||||
) : Opinion<ArticleRef>(id, createdBy, target, choice)
|
||||
@@ -1,20 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityCreatedAtImp
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAt
|
||||
import fr.postgresjson.entity.mutable.EntityDeletedAtImp
|
||||
import java.util.*
|
||||
|
||||
class OpinionChoice(
|
||||
id: UUID? = null,
|
||||
val name: String,
|
||||
val target: List<String>?
|
||||
) : OpinionChoiceRef(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityDeletedAt by EntityDeletedAtImp()
|
||||
|
||||
open class OpinionChoiceRef(
|
||||
id: UUID?
|
||||
) : UuidEntity(id ?: UUID.randomUUID())
|
||||
@@ -1,15 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.EntityI
|
||||
|
||||
class OpinionAggregation(
|
||||
private val underlying: MutableMap<String, Any> = mutableMapOf()
|
||||
) : MutableMap<String, Any> by underlying, EntityI
|
||||
|
||||
interface Opinionable {
|
||||
var opinions: MutableMap<String, Int>
|
||||
}
|
||||
|
||||
class OpinionableImp : Opinionable {
|
||||
override var opinions: MutableMap<String, Int> = mutableMapOf()
|
||||
}
|
||||
@@ -1,41 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.dcproject.entity.UserI.Roles
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import io.ktor.auth.Principal
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
class User(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
username: String,
|
||||
blockedAt: DateTime? = null,
|
||||
override var plainPassword: String? = null,
|
||||
override var roles: List<Roles> = emptyList()
|
||||
) : UserFull, UserBasic(id, username, blockedAt),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityUpdatedAt by EntityUpdatedAtImp()
|
||||
|
||||
open class UserBasic(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override var username: String,
|
||||
override var blockedAt: DateTime? = null
|
||||
) : UserBasicI, UserRef(id)
|
||||
|
||||
open class UserRef(
|
||||
id: UUID = UUID.randomUUID()
|
||||
) : UserI, UuidEntity(id)
|
||||
|
||||
interface UserI : UuidEntityI, Principal {
|
||||
enum class Roles { ROLE_USER, ROLE_ADMIN }
|
||||
}
|
||||
|
||||
interface UserBasicI : UserI {
|
||||
var username: String
|
||||
var blockedAt: DateTime?
|
||||
}
|
||||
|
||||
interface UserFull : UserBasicI, EntityCreatedAt, EntityUpdatedAt {
|
||||
var plainPassword: String?
|
||||
var roles: List<Roles>
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.EntityI
|
||||
import fr.postgresjson.entity.immutable.EntityUpdatedAt
|
||||
import fr.postgresjson.entity.immutable.EntityUpdatedAtImp
|
||||
|
||||
open class ViewAggregation(
|
||||
val total: Int,
|
||||
val unique: Int
|
||||
) : EntityI,
|
||||
EntityUpdatedAt by EntityUpdatedAtImp() {
|
||||
constructor() : this(0, 0)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
interface Viewable {
|
||||
var views: ViewAggregation
|
||||
}
|
||||
|
||||
class ViewableImp : Viewable {
|
||||
override var views: ViewAggregation = ViewAggregation()
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
interface Votable {
|
||||
var votes: VoteAggregation
|
||||
}
|
||||
|
||||
class VotableImp : Votable {
|
||||
override var votes: VoteAggregation = VoteAggregation()
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.immutable.*
|
||||
import java.util.*
|
||||
|
||||
open class Vote<T : TargetI>(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
override val createdBy: CitizenBasic,
|
||||
override var target: T,
|
||||
var note: Int,
|
||||
var anonymous: Boolean = true
|
||||
) : ExtraI<T, CitizenBasicI>,
|
||||
UuidEntity(id),
|
||||
EntityCreatedAt by EntityCreatedAtImp(),
|
||||
EntityCreatedBy<CitizenBasicI> by EntityCreatedByImp(createdBy),
|
||||
EntityUpdatedAt by EntityUpdatedAtImp() {
|
||||
init {
|
||||
if (note > 1 && note < -1) {
|
||||
error("note must be 1, 0 or -1")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
package fr.dcproject.entity
|
||||
|
||||
import fr.postgresjson.entity.EntityI
|
||||
import fr.postgresjson.entity.mutable.EntityUpdatedAt
|
||||
import fr.postgresjson.entity.mutable.EntityUpdatedAtImp
|
||||
|
||||
open class VoteAggregation(
|
||||
val up: Int,
|
||||
val neutral: Int,
|
||||
val down: Int,
|
||||
val total: Int,
|
||||
val score: Int
|
||||
) : EntityI,
|
||||
EntityUpdatedAt by EntityUpdatedAtImp() {
|
||||
constructor() : this(0, 0, 0, 0, 0)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
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? = null,
|
||||
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? = null,
|
||||
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? = null
|
||||
) : UuidEntity(id ?: UUID.randomUUID()), WorkgroupI
|
||||
|
||||
interface WorkgroupWithAuthI<Z : CitizenWithUserI> : WorkgroupWithMembersI<Z>, EntityCreatedBy<Z>, EntityDeletedAt {
|
||||
val anonymous: Boolean
|
||||
val owner: 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 WorkgroupWithMembersI<Z : CitizenI> : WorkgroupI {
|
||||
var members: List<Z>
|
||||
}
|
||||
|
||||
fun List<CitizenI>.asCitizen(citizen: CitizenI): Boolean = this.map { it.id }.contains(citizen.id)
|
||||
|
||||
interface WorkgroupI : UuidEntityI
|
||||
@@ -1,104 +0,0 @@
|
||||
package fr.dcproject.event
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.rabbitmq.client.*
|
||||
import com.rabbitmq.client.BuiltinExchangeType.DIRECT
|
||||
import fr.dcproject.Config
|
||||
import fr.dcproject.entity.Article
|
||||
import fr.dcproject.event.publisher.Publisher
|
||||
import fr.dcproject.repository.Follow
|
||||
import fr.postgresjson.serializer.deserialize
|
||||
import io.ktor.application.EventDefinition
|
||||
import io.lettuce.core.api.async.RedisAsyncCommands
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.io.errors.IOException
|
||||
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
|
||||
|
||||
class ArticleUpdate(
|
||||
target: Article
|
||||
) : EntityEvent(target, "article", "update") {
|
||||
companion object {
|
||||
val event = EventDefinition<ArticleUpdate>()
|
||||
}
|
||||
}
|
||||
|
||||
fun EventSubscriber.Configuration.configEvent(
|
||||
rabbitFactory: ConnectionFactory,
|
||||
redis: RedisAsyncCommands<String, String>,
|
||||
followRepo: FollowArticleRepository,
|
||||
serialiser: ObjectMapper
|
||||
) {
|
||||
/* Config Rabbit */
|
||||
val exchangeName = Config.exchangeNotificationName
|
||||
rabbitFactory.newConnection().use { connection ->
|
||||
connection.createChannel().use { channel ->
|
||||
channel.queueDeclare("push", true, false, false, null)
|
||||
channel.queueDeclare("email", true, false, false, null)
|
||||
channel.exchangeDeclare(exchangeName, DIRECT, true)
|
||||
channel.queueBind("push", exchangeName, "")
|
||||
channel.queueBind("email", exchangeName, "")
|
||||
}
|
||||
}
|
||||
|
||||
/* Declare publisher on event */
|
||||
val publisher = Publisher(serialiser, rabbitFactory)
|
||||
subscribe(ArticleUpdate.event) {
|
||||
publisher.publish(it)
|
||||
}
|
||||
|
||||
/* Launch Consumer */
|
||||
GlobalScope.launch {
|
||||
val rabbitChannel = rabbitFactory.newConnection().createChannel()
|
||||
|
||||
val consumerPush: Consumer = object : DefaultConsumer(rabbitChannel) {
|
||||
@Throws(IOException::class)
|
||||
override fun handleDelivery(
|
||||
consumerTag: String,
|
||||
envelope: Envelope,
|
||||
properties: AMQP.BasicProperties,
|
||||
body: ByteArray
|
||||
) = runBlocking {
|
||||
val message = body.toString(Charsets.UTF_8)
|
||||
val msg =
|
||||
message.deserialize<EntityEvent>() ?: error("Unable to unserialise event message from rabbit")
|
||||
|
||||
let {
|
||||
when (msg.type) {
|
||||
"article" -> followRepo
|
||||
else -> error("event '${msg.type}' not implemented")
|
||||
} as Follow<*, *>
|
||||
}
|
||||
.findFollowsByTarget(msg.target)
|
||||
.collect { follow ->
|
||||
redis.zadd(
|
||||
"notification:${follow.createdBy.id}",
|
||||
msg.id,
|
||||
message
|
||||
)
|
||||
}
|
||||
|
||||
rabbitChannel.basicAck(envelope.deliveryTag, false)
|
||||
}
|
||||
}
|
||||
|
||||
val consumerEmail: Consumer = object : DefaultConsumer(rabbitChannel) {
|
||||
@Throws(IOException::class)
|
||||
override fun handleDelivery(
|
||||
consumerTag: String,
|
||||
envelope: Envelope,
|
||||
properties: AMQP.BasicProperties,
|
||||
body: ByteArray
|
||||
) {
|
||||
val message = body.toString(Charsets.UTF_8)
|
||||
println("The message is receive for send email: $message")
|
||||
// TODO implement email sender
|
||||
rabbitChannel.basicAck(envelope.deliveryTag, false)
|
||||
}
|
||||
}
|
||||
rabbitChannel.basicConsume("push", false, consumerPush) // The front consume the redis via Websocket
|
||||
rabbitChannel.basicConsume("email", false, consumerEmail)
|
||||
}
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package fr.dcproject.event
|
||||
|
||||
import fr.postgresjson.entity.Serializable
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import io.ktor.application.*
|
||||
import io.ktor.util.AttributeKey
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import kotlinx.coroutines.DisposableHandle
|
||||
import org.joda.time.DateTime
|
||||
import kotlin.random.Random.Default.nextInt
|
||||
|
||||
open class Event(
|
||||
val type: String,
|
||||
val createdAt: DateTime = DateTime.now()
|
||||
) : Serializable {
|
||||
val id: Double = randId(createdAt.millis)
|
||||
|
||||
private fun randId(time: Long): Double {
|
||||
return (time.toString() + nextInt(1000, 9999).toString()).toDouble()
|
||||
}
|
||||
}
|
||||
|
||||
open class EntityEvent(
|
||||
val target: UuidEntity,
|
||||
type: String,
|
||||
val action: String
|
||||
) : Event(type)
|
||||
|
||||
/**
|
||||
* Installation Class
|
||||
*/
|
||||
class EventSubscriber {
|
||||
class Configuration(private val monitor: ApplicationEvents) {
|
||||
private val subscribers = mutableListOf<DisposableHandle>()
|
||||
fun <T : Event> subscribe(definition: EventDefinition<T>, handler: EventHandler<T>): DisposableHandle {
|
||||
return monitor.subscribe(definition, handler).also {
|
||||
subscribers.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object Feature : ApplicationFeature<Application, Configuration, EventSubscriber> {
|
||||
override val key = AttributeKey<EventSubscriber>("EventSubscriber")
|
||||
|
||||
@KtorExperimentalAPI
|
||||
override fun install(
|
||||
pipeline: Application,
|
||||
configure: Configuration.() -> Unit
|
||||
): EventSubscriber {
|
||||
Configuration(pipeline.environment.monitor).apply(configure)
|
||||
return EventSubscriber()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
package fr.dcproject.event.publisher
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import com.rabbitmq.client.ConnectionFactory
|
||||
import fr.dcproject.Config
|
||||
import fr.dcproject.event.EntityEvent
|
||||
import kotlinx.coroutines.GlobalScope
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.launch
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
class Publisher(
|
||||
private val mapper: ObjectMapper,
|
||||
private val factory: ConnectionFactory,
|
||||
private val logger: Logger = LoggerFactory.getLogger(Publisher::class.qualifiedName)
|
||||
) {
|
||||
fun <T : EntityEvent> publish(it: T): Job {
|
||||
return GlobalScope.launch {
|
||||
factory.newConnection().use { connection ->
|
||||
connection.createChannel().use { channel ->
|
||||
channel.basicPublish(Config.exchangeNotificationName, "", null, it.serialize().toByteArray())
|
||||
logger.debug("Publish message ${it.target.id}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun EntityEvent.serialize(): String {
|
||||
return mapper.writeValueAsString(this) ?: error("Unable tu serialize message")
|
||||
}
|
||||
}
|
||||
@@ -1,27 +0,0 @@
|
||||
package fr.dcproject.messages
|
||||
|
||||
import com.sendgrid.Method
|
||||
import com.sendgrid.Request
|
||||
import com.sendgrid.SendGrid
|
||||
import com.sendgrid.helpers.mail.Mail
|
||||
import java.io.IOException
|
||||
|
||||
class Mailer(
|
||||
private val key: String
|
||||
) {
|
||||
fun sendEmail(action: () -> Mail): Boolean {
|
||||
val mail = action()
|
||||
|
||||
val sg = SendGrid(key)
|
||||
val request = Request()
|
||||
try {
|
||||
request.method = Method.POST
|
||||
request.endpoint = "mail/send"
|
||||
request.body = mail.build()
|
||||
val response = sg.api(request)
|
||||
return response.statusCode == 202
|
||||
} catch (ex: IOException) {
|
||||
throw ex
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package fr.dcproject.messages
|
||||
|
||||
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
|
||||
|
||||
class SsoManager(
|
||||
private val mailer: Mailer,
|
||||
private val domain: String,
|
||||
private val citizenRepo: CitizenRepository
|
||||
) {
|
||||
fun sendMail(email: String, url: String) {
|
||||
val citizen = citizenRepo.findByEmail(email) ?: noEmail(email)
|
||||
sendMail(citizen, url)
|
||||
}
|
||||
|
||||
fun sendMail(citizen: CitizenBasicI, url: String) {
|
||||
mailer.sendEmail {
|
||||
Mail(
|
||||
Email("sso@$domain"),
|
||||
"Connection",
|
||||
Email(citizen.email),
|
||||
Content("text/plain", generateContent(citizen, url))
|
||||
).apply {
|
||||
addContent(Content("text/html", generateHtmlContent(citizen, url)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateHtmlContent(citizen: CitizenBasicI, url: String): String? {
|
||||
val urlObject = URLBuilder(url)
|
||||
urlObject.parameters.append("token", JwtConfig.makeToken(citizen.user))
|
||||
return "Click <a href=\"${urlObject.buildString()}\">here</a> for connect to $domain"
|
||||
}
|
||||
|
||||
private fun generateContent(citizen: CitizenBasicI, url: String): String {
|
||||
val urlObject = URLBuilder(url)
|
||||
urlObject.parameters.append("token", JwtConfig.makeToken(citizen.user))
|
||||
return "Copy this link into your browser for connect to $domain: \n${urlObject.buildString()}"
|
||||
}
|
||||
|
||||
class EmailNotFound(val email: String) : Exception() {
|
||||
override val message: String = "No Citizen with this email : $email"
|
||||
}
|
||||
|
||||
private fun noEmail(email: String): Nothing = throw EmailNotFound(email)
|
||||
}
|
||||
@@ -1,54 +0,0 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.entity.ArticleFull
|
||||
import fr.dcproject.entity.ArticleSimple
|
||||
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.Article as ArticleEntity
|
||||
|
||||
class Article(override var requester: Requester) : RepositoryI {
|
||||
fun findById(id: UUID): ArticleEntity? {
|
||||
val function = requester.getFunction("find_article_by_id")
|
||||
return function.selectOne("id" to id)
|
||||
}
|
||||
|
||||
fun findVerionsByVersionsId(page: Int = 1, limit: Int = 50, versionId: UUID): Paginated<ArticleEntity> {
|
||||
return requester
|
||||
.getFunction("find_articles_versions_by_version_id")
|
||||
.select(page, limit, "version_id" to versionId)
|
||||
}
|
||||
|
||||
fun find(
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: String? = null,
|
||||
direction: Direction? = null,
|
||||
search: String? = null,
|
||||
filter: Filter = Filter()
|
||||
): Paginated<ArticleSimple> {
|
||||
return requester
|
||||
.getFunction("find_articles")
|
||||
.select(
|
||||
page, limit,
|
||||
"sort" to sort?.toSnakeCase(),
|
||||
"direction" to direction,
|
||||
"search" to search,
|
||||
"filter" to filter
|
||||
)
|
||||
}
|
||||
|
||||
fun upsert(article: ArticleFull): ArticleEntity? {
|
||||
return requester
|
||||
.getFunction("upsert_article")
|
||||
.selectOne("resource" to article)
|
||||
}
|
||||
|
||||
class Filter(
|
||||
val createdById: String? = null
|
||||
) : Parameter
|
||||
}
|
||||
@@ -1,67 +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, withUser: Boolean = false): CitizenEntity? {
|
||||
return requester
|
||||
.getFunction(if (withUser) "find_citizen_by_id_with_user" else "find_citizen_by_id")
|
||||
.selectOne("id" to id)
|
||||
}
|
||||
|
||||
fun findByUser(user: UserI): CitizenEntity? {
|
||||
return requester
|
||||
.getFunction("find_citizen_by_user_id")
|
||||
.selectOne("user_id" to user.id)
|
||||
}
|
||||
|
||||
fun findByUsername(unsername: String): CitizenEntity? {
|
||||
return requester
|
||||
.getFunction("find_citizen_by_username")
|
||||
.selectOne("username" to unsername)
|
||||
}
|
||||
|
||||
fun findByEmail(email: String): CitizenEntity? {
|
||||
return 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> {
|
||||
return requester
|
||||
.getFunction("find_citizens")
|
||||
.select(
|
||||
page, limit,
|
||||
"sort" to sort?.toSnakeCase(),
|
||||
"direction" to direction,
|
||||
"search" to search
|
||||
)
|
||||
}
|
||||
|
||||
fun upsert(citizen: CitizenFull): CitizenEntity? {
|
||||
return requester
|
||||
.getFunction("upsert_citizen")
|
||||
.selectOne("resource" to citizen)
|
||||
}
|
||||
|
||||
fun insertWithUser(citizen: CitizenFull): CitizenEntity? {
|
||||
return requester
|
||||
.getFunction("insert_citizen_with_user")
|
||||
.selectOne("resource" to citizen)
|
||||
}
|
||||
}
|
||||
@@ -1,210 +0,0 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.entity.ConstitutionRef
|
||||
import fr.dcproject.entity.TargetI
|
||||
import fr.dcproject.entity.TargetRef
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.entity.immutable.UuidEntityI
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Citizen as CitizenEntity
|
||||
import fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
|
||||
abstract class Comment<T : TargetI>(override var requester: Requester) : RepositoryI {
|
||||
abstract fun findById(id: UUID): CommentEntity<T>?
|
||||
|
||||
abstract fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<CommentEntity<T>>
|
||||
|
||||
open fun findByParent(
|
||||
parent: CommentEntity<T>,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<CommentEntity<T>> {
|
||||
return findByParent(parent.id, page, limit)
|
||||
}
|
||||
|
||||
open fun findByParent(
|
||||
parentId: UUID,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<CommentEntity<T>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_parent")
|
||||
.select(
|
||||
page, limit,
|
||||
"parent_id" to parentId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
open fun findByTarget(
|
||||
target: UuidEntityI,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: CommentArticle.Sort = CommentArticle.Sort.CREATED_AT
|
||||
): Paginated<CommentEntity<T>> {
|
||||
return findByTarget(target.id, page, limit, sort)
|
||||
}
|
||||
|
||||
open fun findByTarget(
|
||||
targetId: UUID,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: CommentArticle.Sort = CommentArticle.Sort.CREATED_AT
|
||||
): Paginated<CommentEntity<T>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_target")
|
||||
.select(
|
||||
page, limit,
|
||||
"target_id" to targetId,
|
||||
"sort" to sort.sql
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun <I : T> comment(comment: CommentEntity<I>) {
|
||||
requester
|
||||
.getFunction("comment")
|
||||
.sendQuery(
|
||||
"reference" to comment.target.reference,
|
||||
"resource" to comment
|
||||
)
|
||||
}
|
||||
|
||||
fun <I : T> edit(comment: CommentEntity<I>) {
|
||||
requester
|
||||
.getFunction("edit_comment")
|
||||
.sendQuery(
|
||||
"id" to comment.id,
|
||||
"content" to comment.content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class CommentGeneric(requester: Requester) : Comment<TargetRef>(requester) {
|
||||
override fun findById(id: UUID): CommentEntity<TargetRef>? {
|
||||
return requester
|
||||
.getFunction("find_comment_by_id")
|
||||
.selectOne(mapOf("id" to id))
|
||||
}
|
||||
|
||||
override fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<CommentEntity<TargetRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_citizen")
|
||||
.select(
|
||||
page, limit,
|
||||
"created_by_id" to citizen.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findByParent(
|
||||
parentId: UUID,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<CommentEntity<TargetRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_parent")
|
||||
.select(
|
||||
page, limit,
|
||||
"parent_id" to parentId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CommentArticle(requester: Requester) : Comment<ArticleEntity>(requester) {
|
||||
override fun findById(id: UUID): CommentEntity<ArticleEntity>? {
|
||||
return requester
|
||||
.getFunction("find_comment_by_id")
|
||||
.selectOne(mapOf("id" to id))
|
||||
}
|
||||
|
||||
override fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<CommentEntity<ArticleEntity>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_citizen")
|
||||
.select(
|
||||
page, limit,
|
||||
"created_by_id" to citizen.id,
|
||||
"reference" to TargetI.getReference(ArticleRef::class)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findByTarget(
|
||||
target: UuidEntityI,
|
||||
page: Int,
|
||||
limit: Int,
|
||||
sort: Sort
|
||||
): Paginated<CommentEntity<ArticleEntity>> = requester
|
||||
.getFunction("find_comments_by_target")
|
||||
.select(
|
||||
page, limit,
|
||||
"target_id" to target.id,
|
||||
"sort" to sort.sql
|
||||
)
|
||||
|
||||
enum class Sort(val sql: String) {
|
||||
CREATED_AT("created_at"), VOTES("votes");
|
||||
|
||||
companion object {
|
||||
fun fromString(string: String): Sort? {
|
||||
return values().firstOrNull { it.sql == string }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class CommentConstitution(requester: Requester) : Comment<ConstitutionRef>(requester) {
|
||||
override fun findById(id: UUID): CommentEntity<ConstitutionRef>? {
|
||||
return requester
|
||||
.getFunction("find_comment_by_id")
|
||||
.selectOne(mapOf("id" to id))
|
||||
}
|
||||
|
||||
override fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<CommentEntity<ConstitutionRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_citizen")
|
||||
.select(
|
||||
page, limit,
|
||||
"created_by_id" to citizen.id,
|
||||
"reference" to TargetI.getReference(ConstitutionRef::class)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findByTarget(
|
||||
target: UuidEntityI,
|
||||
page: Int,
|
||||
limit: Int,
|
||||
sort: CommentArticle.Sort
|
||||
): Paginated<CommentEntity<ConstitutionRef>> {
|
||||
return requester.run {
|
||||
getFunction("find_comments_by_target")
|
||||
.select(
|
||||
page, limit,
|
||||
"target_id" to target.id,
|
||||
"sort" to sort.sql
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.entity.CitizenSimple
|
||||
import fr.dcproject.entity.ConstitutionSimple
|
||||
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.Constitution as ConstitutionEntity
|
||||
|
||||
class Constitution(override var requester: Requester) : RepositoryI {
|
||||
fun findById(id: UUID): ConstitutionEntity? {
|
||||
val function = requester.getFunction("find_constitution_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
|
||||
): Paginated<ConstitutionEntity> {
|
||||
return requester
|
||||
.getFunction("find_constitutions")
|
||||
.select(
|
||||
page, limit,
|
||||
"sort" to sort?.toSnakeCase(),
|
||||
"direction" to direction,
|
||||
"search" to search
|
||||
)
|
||||
}
|
||||
|
||||
fun upsert(constitution: ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleRef>>): ConstitutionEntity? {
|
||||
return requester
|
||||
.getFunction("upsert_constitution")
|
||||
.selectOne("resource" to constitution)
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.entity.*
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
import fr.dcproject.entity.Constitution as ConstitutionEntity
|
||||
import fr.dcproject.entity.Follow as FollowEntity
|
||||
|
||||
sealed class Follow<IN : TargetRef, OUT : TargetRef>(override var requester: Requester) : RepositoryI {
|
||||
open fun findByCitizen(
|
||||
citizen: CitizenI,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<FollowEntity<OUT>> =
|
||||
findByCitizen(citizen.id, page, limit)
|
||||
|
||||
open fun findByCitizen(
|
||||
citizenId: UUID,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<FollowEntity<OUT>> {
|
||||
return requester
|
||||
.getFunction("find_follows_by_citizen")
|
||||
.select(
|
||||
page, limit,
|
||||
"created_by_id" to citizenId
|
||||
)
|
||||
}
|
||||
|
||||
fun follow(follow: FollowEntity<IN>) {
|
||||
requester
|
||||
.getFunction("follow")
|
||||
.sendQuery(
|
||||
"reference" to follow.target.reference,
|
||||
"target_id" to follow.target.id,
|
||||
"created_by_id" to follow.createdBy.id
|
||||
)
|
||||
}
|
||||
|
||||
fun unfollow(follow: FollowEntity<IN>) {
|
||||
requester
|
||||
.getFunction("unfollow")
|
||||
.sendQuery(
|
||||
"reference" to follow.target.reference,
|
||||
"target_id" to follow.target.id,
|
||||
"created_by_id" to follow.createdBy.id
|
||||
)
|
||||
}
|
||||
|
||||
open fun findFollow(
|
||||
citizen: CitizenI,
|
||||
target: TargetRef
|
||||
): FollowEntity<OUT>? =
|
||||
requester
|
||||
.getFunction("find_follow")
|
||||
.selectOne(
|
||||
"citizen_id" to citizen.id,
|
||||
"target_id" to target.id,
|
||||
"target_reference" to target.reference
|
||||
)
|
||||
|
||||
fun findFollowsByTarget(
|
||||
target: UuidEntity,
|
||||
bulkSize: Int = 300
|
||||
): Flow<FollowSimple<IN, CitizenRef>> = flow {
|
||||
var nextPage = 1
|
||||
do {
|
||||
val paginate = findFollowsByTarget(target, nextPage, bulkSize)
|
||||
paginate.result.forEach {
|
||||
emit(it)
|
||||
}
|
||||
nextPage = paginate.currentPage + 1
|
||||
} while (!paginate.isLastPage())
|
||||
}
|
||||
|
||||
abstract fun findFollowsByTarget(
|
||||
target: UuidEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 300
|
||||
): Paginated<FollowSimple<IN, CitizenRef>>
|
||||
}
|
||||
|
||||
class FollowArticle(requester: Requester) : Follow<ArticleRef, ArticleEntity>(requester) {
|
||||
override fun findByCitizen(
|
||||
citizenId: UUID,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<FollowEntity<ArticleEntity>> {
|
||||
return requester.run {
|
||||
getFunction("find_follows_article_by_citizen")
|
||||
.select(
|
||||
page, limit,
|
||||
"created_by_id" to citizenId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findFollowsByTarget(
|
||||
target: UuidEntity,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<FollowSimple<ArticleRef, CitizenRef>> {
|
||||
return requester
|
||||
.getFunction("find_follows_article_by_target")
|
||||
.select(
|
||||
page, limit,
|
||||
"target_id" to target.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class FollowConstitution(requester: Requester) : Follow<ConstitutionRef, ConstitutionEntity>(requester) {
|
||||
override fun findByCitizen(
|
||||
citizenId: UUID,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<FollowEntity<ConstitutionEntity>> {
|
||||
return requester.run {
|
||||
getFunction("find_follows_constitution_by_citizen")
|
||||
.select(
|
||||
page, limit,
|
||||
"created_by_id" to citizenId
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun findFollowsByTarget(
|
||||
target: UuidEntity,
|
||||
page: Int,
|
||||
limit: Int
|
||||
): Paginated<FollowSimple<ConstitutionRef, CitizenRef>> {
|
||||
TODO("Not yet implemented")
|
||||
}
|
||||
}
|
||||
@@ -1,145 +0,0 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.OpinionChoiceRef
|
||||
import fr.dcproject.entity.TargetRef
|
||||
import fr.postgresjson.connexion.Paginated
|
||||
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.entity.Opinion as OpinionEntity
|
||||
import fr.dcproject.entity.OpinionArticle as OpinionArticleEntity
|
||||
import fr.dcproject.entity.OpinionChoice as OpinionChoiceEntity
|
||||
|
||||
open class OpinionChoice(override val requester: Requester) : RepositoryI {
|
||||
/**
|
||||
* find all opinion choices
|
||||
* can be filtered by target compatibility
|
||||
*/
|
||||
fun findOpinionsChoices(targets: List<String> = emptyList()): List<OpinionChoiceEntity> =
|
||||
requester
|
||||
.getFunction("find_opinion_choices")
|
||||
.select(
|
||||
"targets" to targets
|
||||
)
|
||||
|
||||
/**
|
||||
* find opinion choices by name
|
||||
*/
|
||||
fun findOpinionsChoiceByName(name: String): OpinionChoiceEntity? =
|
||||
findOpinionsChoices().first {
|
||||
it.name == name
|
||||
}
|
||||
|
||||
/**
|
||||
* find one opinion choices by id
|
||||
*/
|
||||
fun findOpinionChoiceById(id: UUID): OpinionChoiceEntity? =
|
||||
requester
|
||||
.getFunction("find_opinion_choice_by_id")
|
||||
.selectOne(
|
||||
"id" to id
|
||||
)
|
||||
|
||||
/**
|
||||
* find one opinion choices by id
|
||||
*/
|
||||
fun findOpinionChoicesByIds(ids: List<UUID>): List<OpinionChoiceEntity> =
|
||||
requester
|
||||
.getFunction("find_opinion_choices_by_ids")
|
||||
.select(
|
||||
"ids" to ids
|
||||
)
|
||||
|
||||
fun upsertOpinionChoice(opinionChoice: OpinionChoiceEntity): OpinionChoiceEntity = requester
|
||||
.getFunction("upsert_opinion_choice")
|
||||
.selectOne(
|
||||
"resource" to opinionChoice
|
||||
)!!
|
||||
}
|
||||
|
||||
abstract class Opinion<T : TargetRef>(requester: Requester) : OpinionChoice(requester) {
|
||||
/**
|
||||
* Create an Opinion on target (article,...)
|
||||
*/
|
||||
abstract fun updateOpinions(choices: List<OpinionChoiceRef>, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>>
|
||||
fun updateOpinions(choice: OpinionChoiceRef, citizen: CitizenRef, target: TargetRef): List<OpinionEntity<T>> =
|
||||
updateOpinions(listOf(choice), citizen, target)
|
||||
|
||||
/**
|
||||
* Find opinions of one citizen filtered by target ids
|
||||
*/
|
||||
fun findCitizenOpinionsByTargets(
|
||||
citizen: CitizenEntity,
|
||||
targets: List<UUID>
|
||||
): List<OpinionEntity<T>> {
|
||||
val typeReference = object : TypeReference<List<OpinionEntity<T>>>() {}
|
||||
return requester.run {
|
||||
getFunction("find_citizen_opinions_by_target_ids")
|
||||
.select(
|
||||
typeReference, mapOf(
|
||||
"citizen_id" to citizen.id,
|
||||
"ids" to targets
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* find opinion of citizen filtered by one target id
|
||||
*/
|
||||
fun findCitizenOpinionsByTarget(
|
||||
citizen: CitizenEntity,
|
||||
target: UUID
|
||||
): List<OpinionEntity<T>> {
|
||||
val typeReference = object : TypeReference<List<OpinionEntity<T>>>() {}
|
||||
return requester
|
||||
.getFunction("find_citizen_opinions_by_target_id")
|
||||
.select(
|
||||
typeReference, mapOf(
|
||||
"citizen_id" to citizen.id,
|
||||
"id" to target
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* find paginated opinion of one citizen
|
||||
* can be sorted
|
||||
*/
|
||||
fun findCitizenOpinions(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
sort: String? = null,
|
||||
direction: RepositoryI.Direction? = null
|
||||
): Paginated<OpinionEntity<TargetRef>> {
|
||||
return requester
|
||||
.getFunction("find_citizen_opinions")
|
||||
.select(page, limit,
|
||||
"sort" to sort?.toSnakeCase(),
|
||||
"direction" to direction,
|
||||
"citizen_id" to citizen.id
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
class OpinionArticle(requester: Requester) : Opinion<ArticleRef>(requester) {
|
||||
/**
|
||||
* Create an Opinions on Article
|
||||
*/
|
||||
override fun updateOpinions(choices: List<OpinionChoiceRef>, citizen: CitizenRef, target: TargetRef): List<OpinionArticleEntity> {
|
||||
return requester
|
||||
.getFunction("update_citizen_opinions_by_target_id")
|
||||
.select(
|
||||
"choices_ids" to choices.map { it.id },
|
||||
"citizen_id" to citizen.id,
|
||||
"target_id" to target.id,
|
||||
"target_reference" to target.reference
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,43 +0,0 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.entity.UserFull
|
||||
import fr.postgresjson.connexion.Requester
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import io.ktor.auth.UserPasswordCredential
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.User as UserEntity
|
||||
|
||||
class User(override var requester: Requester) : RepositoryI {
|
||||
fun findByCredentials(credentials: UserPasswordCredential): UserEntity? {
|
||||
return requester
|
||||
.getFunction("check_user")
|
||||
.selectOne(
|
||||
"username" to credentials.name,
|
||||
"plain_password" to credentials.password
|
||||
)
|
||||
}
|
||||
|
||||
fun findById(id: UUID): UserEntity {
|
||||
return requester
|
||||
.getFunction("find_user_by_id")
|
||||
.selectOne(
|
||||
"id" to id
|
||||
) ?: throw UserNotFound(id)
|
||||
}
|
||||
|
||||
fun insert(user: UserEntity): UserEntity? {
|
||||
return requester
|
||||
.getFunction("insert_user")
|
||||
.selectOne("resource" to user)
|
||||
}
|
||||
|
||||
fun changePassword(user: UserFull) {
|
||||
requester
|
||||
.getFunction("change_user_password")
|
||||
.sendQuery("resource" to user)
|
||||
}
|
||||
|
||||
class UserNotFound(override val message: String?, override val cause: Throwable?) : Throwable(message, cause) {
|
||||
constructor(id: UUID) : this("No User with ID $id", null)
|
||||
}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.entity.Article
|
||||
import fr.dcproject.entity.Comment
|
||||
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.entity.Vote as VoteEntity
|
||||
|
||||
open class Vote<T : TargetI>(override var requester: Requester) : RepositoryI {
|
||||
fun vote(vote: VoteEntity<T>): VoteAggregation {
|
||||
val author = vote.createdBy
|
||||
val anonymous = author.voteAnonymous
|
||||
return requester
|
||||
.getFunction("vote")
|
||||
.selectOne(
|
||||
"reference" to vote.target.reference,
|
||||
"target_id" to vote.target.id,
|
||||
"note" to vote.note,
|
||||
"created_by_id" to author.id,
|
||||
"anonymous" to anonymous
|
||||
)!!
|
||||
}
|
||||
|
||||
fun findByCitizen(
|
||||
citizenId: UUID,
|
||||
target: String,
|
||||
typeReference: TypeReference<List<VoteEntity<T>>>,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<VoteEntity<T>> {
|
||||
return requester.run {
|
||||
getFunction("find_votes_by_citizen")
|
||||
.select(
|
||||
page, limit, typeReference, mapOf(
|
||||
"created_by_id" to citizenId,
|
||||
"reference" to target
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun findCitizenVotesByTargets(
|
||||
citizen: CitizenEntity,
|
||||
targets: List<UUID>
|
||||
): List<VoteEntity<*>> {
|
||||
val typeReference = object : TypeReference<List<VoteEntity<TargetRef>>>() {}
|
||||
return requester.run {
|
||||
getFunction("find_citizen_votes_by_target_ids")
|
||||
.select(
|
||||
typeReference, mapOf(
|
||||
"citizen_id" to citizen.id,
|
||||
"ids" to targets
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class VoteArticle(requester: Requester) : Vote<Article>(requester) {
|
||||
fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<VoteEntity<Article>> =
|
||||
findByCitizen(
|
||||
citizen.id,
|
||||
"article",
|
||||
object : TypeReference<List<VoteEntity<Article>>>() {},
|
||||
page,
|
||||
limit
|
||||
)
|
||||
}
|
||||
|
||||
class VoteArticleComment(requester: Requester) : Vote<Comment<Article>>(requester) {
|
||||
fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<VoteEntity<Comment<Article>>> =
|
||||
findByCitizen(
|
||||
citizen.id,
|
||||
"article",
|
||||
object : TypeReference<List<VoteEntity<Comment<Article>>>>() {},
|
||||
page,
|
||||
limit
|
||||
)
|
||||
}
|
||||
|
||||
class VoteComment(requester: Requester) : Vote<Comment<TargetRef>>(requester) {
|
||||
fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<VoteEntity<Comment<TargetRef>>> =
|
||||
findByCitizen(
|
||||
citizen.id,
|
||||
"article",
|
||||
object : TypeReference<List<VoteEntity<Comment<TargetRef>>>>() {},
|
||||
page,
|
||||
limit
|
||||
)
|
||||
}
|
||||
|
||||
class VoteConstitution(requester: Requester) : Vote<Constitution>(requester) {
|
||||
fun findByCitizen(
|
||||
citizen: CitizenEntity,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
): Paginated<VoteEntity<Constitution>> =
|
||||
findByCitizen(
|
||||
citizen.id,
|
||||
"constitution",
|
||||
object : TypeReference<List<VoteEntity<Constitution>>>() {},
|
||||
page,
|
||||
limit
|
||||
)
|
||||
}
|
||||
@@ -1,85 +0,0 @@
|
||||
package fr.dcproject.repository
|
||||
|
||||
import fr.dcproject.entity.*
|
||||
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 fr.postgresjson.serializer.serialize
|
||||
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")
|
||||
|
||||
fun delete(workgroup: WorkgroupRef) = requester
|
||||
.getFunction("delete_workgroup")
|
||||
.perform("id" to workgroup.id)
|
||||
|
||||
fun addMember(workgroup: WorkgroupI, member: CitizenI): List<CitizenBasic> =
|
||||
addMembers(workgroup, listOf(member))
|
||||
|
||||
fun addMembers(workgroup: WorkgroupI, members: List<CitizenI>): List<CitizenBasic> = requester
|
||||
.getFunction("add_workgroup_members")
|
||||
.select(
|
||||
"id" to workgroup.id,
|
||||
"resource" to members.serialize()
|
||||
)
|
||||
|
||||
fun removeMember(workgroup: WorkgroupI, memberToDelete: CitizenI): List<CitizenBasic> =
|
||||
removeMembers(workgroup, listOf(memberToDelete))
|
||||
|
||||
fun removeMembers(workgroup: WorkgroupI, membersToDelete: List<CitizenI>): List<CitizenBasic> = requester
|
||||
.getFunction("remove_workgroup_members")
|
||||
.select(
|
||||
"id" to workgroup.id,
|
||||
"resource" to membersToDelete
|
||||
)
|
||||
|
||||
fun updateMembers(workgroup: WorkgroupI, members: List<CitizenI>): List<CitizenBasic> = requester
|
||||
.getFunction("update_workgroup_members")
|
||||
.select(
|
||||
"id" to workgroup.id,
|
||||
"resource" to members
|
||||
)
|
||||
|
||||
fun <W : WorkgroupWithMembersI<CitizenI>> updateMembers(workgroup: W): W {
|
||||
updateMembers(workgroup, workgroup.members).let {
|
||||
workgroup.members = it
|
||||
}
|
||||
|
||||
return workgroup
|
||||
}
|
||||
|
||||
class Filter(
|
||||
val createdById: String? = null
|
||||
) : Parameter
|
||||
}
|
||||
@@ -1,125 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.citizenOrNull
|
||||
import fr.dcproject.event.ArticleUpdate
|
||||
import fr.dcproject.repository.Article.Filter
|
||||
import fr.dcproject.security.voter.ArticleVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.ArticleVoter.Action.VIEW
|
||||
import fr.dcproject.views.ArticleViewManager
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.application
|
||||
import io.ktor.application.call
|
||||
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 kotlinx.coroutines.launch
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
import fr.dcproject.repository.Article as ArticleRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object ArticlesPaths {
|
||||
@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 page: Int = if (page < 1) 1 else page
|
||||
val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
|
||||
}
|
||||
|
||||
@Location("/articles/{article}")
|
||||
class ArticleRequest(val article: ArticleEntity)
|
||||
|
||||
@Location("/articles/{article}/versions")
|
||||
class ArticleVersionsRequest(
|
||||
val article: ArticleEntity,
|
||||
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
|
||||
}
|
||||
|
||||
@Location("/articles")
|
||||
class PostArticleRequest {
|
||||
class Article(
|
||||
val id: UUID?,
|
||||
val title: String,
|
||||
val anonymous: Boolean = true,
|
||||
val content: String,
|
||||
val description: String,
|
||||
val tags: List<String> = emptyList(),
|
||||
val draft: Boolean = false,
|
||||
val versionId: UUID?
|
||||
)
|
||||
|
||||
suspend fun getNewArticle(call: ApplicationCall): ArticleEntity = call.receive<Article>().run {
|
||||
ArticleEntity(
|
||||
id ?: UUID.randomUUID(),
|
||||
title,
|
||||
anonymous,
|
||||
content,
|
||||
description,
|
||||
tags,
|
||||
draft,
|
||||
createdBy = call.citizen
|
||||
).also {
|
||||
it.versionId = versionId ?: UUID.randomUUID()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.article(repo: ArticleRepository, viewManager: ArticleViewManager) {
|
||||
get<ArticlesPaths.ArticlesRequest> {
|
||||
val articles =
|
||||
repo.find(it.page, it.limit, it.sort, it.direction, it.search, Filter(createdById = it.createdBy))
|
||||
assertCan(VIEW, articles.result)
|
||||
call.respond(articles)
|
||||
}
|
||||
|
||||
get<ArticlesPaths.ArticleRequest> {
|
||||
assertCan(VIEW, it.article)
|
||||
|
||||
it.article.views = viewManager.getViewsCount(it.article)
|
||||
|
||||
call.respond(it.article)
|
||||
|
||||
launch {
|
||||
viewManager.addView(call.request.local.remoteHost, it.article, citizenOrNull)
|
||||
}
|
||||
}
|
||||
|
||||
get<ArticlesPaths.ArticleVersionsRequest> {
|
||||
assertCan(VIEW, it.article)
|
||||
|
||||
repo.findVerionsByVersionsId(it.page, it.limit, it.article.versionId).let {
|
||||
call.respond(it)
|
||||
}
|
||||
}
|
||||
|
||||
post<ArticlesPaths.PostArticleRequest> {
|
||||
it.getNewArticle(call).also { article ->
|
||||
assertCan(CREATE, article)
|
||||
repo.upsert(article)
|
||||
call.respond(article)
|
||||
application.environment.monitor.raise(ArticleUpdate.event, ArticleUpdate(article))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
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.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 fr.dcproject.repository.User as UserRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object AuthPaths {
|
||||
@Location("/login")
|
||||
class LoginRequest
|
||||
|
||||
@Location("/register")
|
||||
class RegisterRequest
|
||||
|
||||
@Location("/sso")
|
||||
class SsoRequest {
|
||||
data class Content(val email: String, val url: String)
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@KtorExperimentalAPI
|
||||
fun Route.auth(
|
||||
userRepo: UserRepository,
|
||||
citizenRepo: CitizenRepository,
|
||||
ssoManager: SsoManager
|
||||
) {
|
||||
post<LoginRequest> {
|
||||
try {
|
||||
val credentials = call.receive<UserPasswordCredential>()
|
||||
val user = userRepo.findByCredentials(credentials) ?: throw WrongLoginOrPassword()
|
||||
call.respondText(JwtConfig.makeToken(user))
|
||||
} catch (e: MismatchedInputException) {
|
||||
call.respond(HttpStatusCode.BadRequest, "You must be send name and password to the request")
|
||||
} catch (e: WrongLoginOrPassword) {
|
||||
call.respond(HttpStatusCode.BadRequest, e.message)
|
||||
}
|
||||
}
|
||||
|
||||
post<RegisterRequest> {
|
||||
try {
|
||||
val citizen = call.receive<CitizenEntity>()
|
||||
citizen.user.roles = listOf(ROLE_USER)
|
||||
val created = citizenRepo.insertWithUser(citizen)?.user ?: throw BadRequestException("Bad request")
|
||||
call.respondText(JwtConfig.makeToken(created))
|
||||
} catch (e: MissingKotlinParameterException) {
|
||||
call.respond(HttpStatusCode.BadRequest)
|
||||
}
|
||||
}
|
||||
|
||||
post<SsoRequest> {
|
||||
val content = call.receive<SsoRequest.Content>()
|
||||
try {
|
||||
ssoManager.sendMail(content.email, content.url)
|
||||
} catch (e: SsoManager.EmailNotFound) {
|
||||
call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
}
|
||||
|
||||
class WrongLoginOrPassword(override val message: String = "Username not exist or password is wrong") : Exception()
|
||||
@@ -1,94 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import com.fasterxml.jackson.module.kotlin.MissingKotlinParameterException
|
||||
import fr.dcproject.citizen
|
||||
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.postgresjson.repository.RepositoryI.Direction
|
||||
import io.ktor.application.call
|
||||
import io.ktor.auth.UserPasswordCredential
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
import io.ktor.locations.get
|
||||
import io.ktor.locations.put
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
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)
|
||||
assertCan(VIEW, citizens.result)
|
||||
call.respond(citizens)
|
||||
}
|
||||
|
||||
get<CitizenRequest> {
|
||||
assertCan(VIEW, it.citizen)
|
||||
|
||||
call.respond(it.citizen)
|
||||
}
|
||||
|
||||
get<CurrentCitizenRequest> {
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,89 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Comment
|
||||
import fr.dcproject.entity.CommentRef
|
||||
import fr.dcproject.routes.CommentPaths.CreateCommentRequest.Content
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.*
|
||||
import fr.ktorVoter.assertCan
|
||||
import io.ktor.application.call
|
||||
import io.ktor.features.NotFoundException
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.request.receiveText
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import java.util.*
|
||||
import fr.dcproject.repository.CommentGeneric as CommentRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object CommentPaths {
|
||||
@Location("/comments/{comment}")
|
||||
class CommentRequest(val comment: CommentRef)
|
||||
|
||||
@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
|
||||
}
|
||||
|
||||
@Location("/comments/{comment}/children")
|
||||
class CreateCommentRequest(val comment: CommentRef) {
|
||||
class Content(val content: String)
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalAPI
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.comment(repo: CommentRepository) {
|
||||
get<CommentPaths.CommentRequest> {
|
||||
val comment = repo.findById(it.comment.id)!!
|
||||
assertCan(VIEW, comment)
|
||||
|
||||
call.respond(HttpStatusCode.OK, comment)
|
||||
}
|
||||
|
||||
get<CommentPaths.CommentChildrenRequest> {
|
||||
val comments =
|
||||
repo.findByParent(
|
||||
it.comment,
|
||||
it.page,
|
||||
it.limit
|
||||
)
|
||||
|
||||
assertCan(VIEW, comments.result)
|
||||
|
||||
call.respond(HttpStatusCode.OK, comments)
|
||||
}
|
||||
|
||||
post<CommentPaths.CreateCommentRequest> {
|
||||
val parent = repo.findById(it.comment.id) ?: throw NotFoundException("Comment not found")
|
||||
val newComment = Comment(
|
||||
content = call.receive<Content>().content,
|
||||
createdBy = citizen,
|
||||
parent = parent
|
||||
)
|
||||
|
||||
assertCan(CREATE, newComment)
|
||||
repo.comment(newComment)
|
||||
|
||||
call.respond(HttpStatusCode.Created, newComment)
|
||||
}
|
||||
|
||||
put<CommentPaths.CommentRequest> {
|
||||
val comment = repo.findById(it.comment.id)!!
|
||||
assertCan(UPDATE, comment)
|
||||
|
||||
comment.content = call.receiveText()
|
||||
repo.edit(comment)
|
||||
|
||||
call.respond(HttpStatusCode.OK, comment)
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Article
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.repository.CommentArticle.Sort
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
|
||||
import fr.ktorVoter.assertCan
|
||||
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 fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.repository.CommentArticle as CommentArticleRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object CommentArticlePaths {
|
||||
@Location("/articles/{article}/comments")
|
||||
class ArticleCommentRequest(
|
||||
val article: Article,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
val search: String? = null,
|
||||
sort: String = 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: Sort = Sort.fromString(sort) ?: Sort.CREATED_AT
|
||||
}
|
||||
|
||||
@Location("/articles/{article}/comments")
|
||||
class PostArticleCommentRequest(
|
||||
val article: Article
|
||||
) {
|
||||
class Comment(
|
||||
val content: String
|
||||
)
|
||||
|
||||
suspend fun getComment(call: ApplicationCall) = call.receive<Comment>().run {
|
||||
CommentEntity(
|
||||
target = article,
|
||||
createdBy = call.citizen,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Location("/citizens/{citizen}/comments/articles")
|
||||
class CitizenCommentArticleRequest(val citizen: Citizen)
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.commentArticle(repo: CommentArticleRepository) {
|
||||
get<CommentArticlePaths.ArticleCommentRequest> {
|
||||
val comment = repo.findByTarget(it.article, it.page, it.limit, it.sort)
|
||||
if (comment.result.isNotEmpty()) {
|
||||
assertCan(VIEW, comment.result)
|
||||
}
|
||||
call.respond(HttpStatusCode.OK, comment)
|
||||
}
|
||||
|
||||
post<CommentArticlePaths.PostArticleCommentRequest> {
|
||||
it.getComment(call).let { comment ->
|
||||
assertCan(CREATE, comment)
|
||||
repo.comment(comment)
|
||||
call.respond(HttpStatusCode.Created, comment)
|
||||
}
|
||||
}
|
||||
|
||||
get<CommentArticlePaths.CitizenCommentArticleRequest> {
|
||||
repo.findByCitizen(it.citizen).let { comments ->
|
||||
assertCan(VIEW, comments.result)
|
||||
call.respond(comments)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.entity.ConstitutionRef
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.CommentVoter.Action.VIEW
|
||||
import fr.ktorVoter.assertCan
|
||||
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.receiveText
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.repository.CommentConstitution as CommentConstitutionRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object CommentConstitutionPaths {
|
||||
@Location("/constitutions/{constitution}/comments")
|
||||
class ConstitutionCommentRequest(val constitution: ConstitutionRef)
|
||||
|
||||
@Location("/citizens/{citizen}/comments/constitutions")
|
||||
class CitizenCommentConstitutionRequest(val citizen: Citizen)
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.commentConstitution(repo: CommentConstitutionRepository) {
|
||||
get<CommentConstitutionPaths.ConstitutionCommentRequest> {
|
||||
val comments = repo.findByTarget(it.constitution)
|
||||
assertCan(VIEW, comments.result)
|
||||
call.respond(HttpStatusCode.OK, comments)
|
||||
}
|
||||
|
||||
post<CommentConstitutionPaths.ConstitutionCommentRequest> {
|
||||
val content = call.receiveText()
|
||||
val comment = CommentEntity(
|
||||
target = it.constitution,
|
||||
createdBy = citizen,
|
||||
content = content
|
||||
)
|
||||
assertCan(CREATE, comment)
|
||||
repo.comment(comment)
|
||||
|
||||
call.respond(HttpStatusCode.Created, comment)
|
||||
}
|
||||
|
||||
get<CommentConstitutionPaths.CitizenCommentConstitutionRequest> {
|
||||
val comments = repo.findByCitizen(it.citizen)
|
||||
assertCan(VIEW, comments.result)
|
||||
call.respond(comments)
|
||||
}
|
||||
}
|
||||
@@ -1,105 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.entity.CitizenSimple
|
||||
import fr.dcproject.entity.ConstitutionSimple
|
||||
import fr.dcproject.security.voter.ConstitutionVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.ConstitutionVoter.Action.VIEW
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.postgresjson.entity.immutable.UuidEntity
|
||||
import fr.postgresjson.repository.RepositoryI
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.application.call
|
||||
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 java.util.*
|
||||
import fr.dcproject.entity.Constitution as ConstitutionEntity
|
||||
import fr.dcproject.repository.Constitution as ConstitutionRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object ConstitutionPaths {
|
||||
@Location("/constitutions")
|
||||
class ConstitutionsRequest(
|
||||
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
|
||||
}
|
||||
|
||||
@Location("/constitutions/{constitution}")
|
||||
class ConstitutionRequest(val constitution: ConstitutionEntity)
|
||||
|
||||
@Location("/constitutions")
|
||||
class PostConstitutionRequest {
|
||||
class Constitution(
|
||||
var title: String,
|
||||
var anonymous: Boolean = true,
|
||||
var titles: MutableList<Title> = mutableListOf(),
|
||||
var draft: Boolean = false,
|
||||
var lastVersion: Boolean = false,
|
||||
var versionId: UUID = UUID.randomUUID()
|
||||
) {
|
||||
init {
|
||||
titles.forEachIndexed { index, title ->
|
||||
title.rank = index
|
||||
}
|
||||
}
|
||||
|
||||
class Title(
|
||||
id: UUID = UUID.randomUUID(),
|
||||
var name: String,
|
||||
var rank: Int? = null,
|
||||
var articles: MutableList<ArticleRef> = mutableListOf()
|
||||
) : UuidEntity(id) {
|
||||
fun create(): ConstitutionSimple.TitleSimple<ArticleRef> =
|
||||
ConstitutionSimple.TitleSimple(
|
||||
id, name, rank, articles
|
||||
)
|
||||
}
|
||||
|
||||
fun List<Title>.create(): MutableList<ConstitutionSimple.TitleSimple<ArticleRef>> =
|
||||
map { it.create() }.toMutableList()
|
||||
}
|
||||
|
||||
suspend fun getNewConstitution(call: ApplicationCall): ConstitutionSimple<CitizenSimple, ConstitutionSimple.TitleSimple<ArticleRef>> = call.receive<Constitution>().run {
|
||||
ConstitutionSimple(
|
||||
title = title,
|
||||
titles = titles.create(),
|
||||
createdBy = call.citizen,
|
||||
versionId = versionId
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.constitution(repo: ConstitutionRepository) {
|
||||
get<ConstitutionPaths.ConstitutionsRequest> {
|
||||
val constitutions = repo.find(it.page, it.limit, it.sort, it.direction, it.search)
|
||||
assertCan(VIEW, constitutions.result)
|
||||
call.respond(constitutions)
|
||||
}
|
||||
|
||||
get<ConstitutionPaths.ConstitutionRequest> {
|
||||
assertCan(VIEW, it.constitution)
|
||||
call.respond(it.constitution)
|
||||
}
|
||||
|
||||
post<ConstitutionPaths.PostConstitutionRequest> {
|
||||
it.getNewConstitution(call).let { constitution ->
|
||||
assertCan(CREATE, constitution)
|
||||
repo.upsert(constitution)
|
||||
call.respond(constitution)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.ArticleRef
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.*
|
||||
import fr.ktorVoter.assertCan
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import fr.dcproject.entity.Follow as FollowEntity
|
||||
import fr.dcproject.repository.FollowArticle as FollowArticleRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object FollowArticlePaths {
|
||||
@Location("/articles/{article}/follows")
|
||||
class ArticleFollowRequest(val article: ArticleRef)
|
||||
|
||||
@Location("/citizens/{citizen}/follows/articles")
|
||||
class CitizenFollowArticleRequest(val citizen: Citizen)
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.followArticle(repo: FollowArticleRepository) {
|
||||
post<FollowArticlePaths.ArticleFollowRequest> {
|
||||
val follow = FollowEntity(target = it.article, createdBy = this.citizen)
|
||||
assertCan(CREATE, follow)
|
||||
repo.follow(follow)
|
||||
call.respond(HttpStatusCode.Created)
|
||||
}
|
||||
|
||||
delete<FollowArticlePaths.ArticleFollowRequest> {
|
||||
val follow = FollowEntity(target = it.article, createdBy = this.citizen)
|
||||
assertCan(DELETE, follow)
|
||||
repo.unfollow(follow)
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
|
||||
get<FollowArticlePaths.ArticleFollowRequest> {
|
||||
repo.findFollow(citizen, it.article)?.let { follow ->
|
||||
assertCan(VIEW, follow)
|
||||
call.respond(follow)
|
||||
} ?: call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
|
||||
get<FollowArticlePaths.CitizenFollowArticleRequest> {
|
||||
val follows = repo.findByCitizen(it.citizen)
|
||||
if (follows.result.isNotEmpty()) {
|
||||
assertCan(VIEW, follows.result)
|
||||
}
|
||||
call.respond(follows)
|
||||
}
|
||||
}
|
||||
@@ -1,53 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.ConstitutionRef
|
||||
import fr.dcproject.security.voter.FollowVoter.Action.*
|
||||
import fr.ktorVoter.assertCan
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.*
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import fr.dcproject.entity.Follow as FollowEntity
|
||||
import fr.dcproject.repository.FollowConstitution as FollowConstitutionRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object FollowConstitutionPaths {
|
||||
@Location("/constitutions/{constitution}/follows")
|
||||
class ConstitutionFollowRequest(val constitution: ConstitutionRef)
|
||||
|
||||
@Location("/citizens/{citizen}/follows/constitutions")
|
||||
class CitizenFollowConstitutionRequest(val citizen: CitizenRef)
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.followConstitution(repo: FollowConstitutionRepository) {
|
||||
post<FollowConstitutionPaths.ConstitutionFollowRequest> {
|
||||
val follow = FollowEntity(target = it.constitution, createdBy = this.citizen)
|
||||
assertCan(CREATE, follow)
|
||||
repo.follow(follow)
|
||||
call.respond(HttpStatusCode.Created)
|
||||
}
|
||||
|
||||
delete<FollowConstitutionPaths.ConstitutionFollowRequest> {
|
||||
val follow = FollowEntity(target = it.constitution, createdBy = this.citizen)
|
||||
assertCan(DELETE, follow)
|
||||
repo.unfollow(follow)
|
||||
call.respond(HttpStatusCode.NoContent)
|
||||
}
|
||||
|
||||
get<FollowConstitutionPaths.ConstitutionFollowRequest> {
|
||||
repo.findFollow(citizen, it.constitution)?.let { follow ->
|
||||
assertCan(VIEW, follow)
|
||||
call.respond(follow)
|
||||
} ?: call.respond(HttpStatusCode.NotFound)
|
||||
}
|
||||
|
||||
get<FollowConstitutionPaths.CitizenFollowConstitutionRequest> {
|
||||
val follows = repo.findByCitizen(it.citizen)
|
||||
assertCan(VIEW, follows.result)
|
||||
call.respond(follows)
|
||||
}
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.event.Event
|
||||
import fr.postgresjson.serializer.deserialize
|
||||
import io.ktor.client.HttpClient
|
||||
import io.ktor.http.cio.websocket.Frame
|
||||
import io.ktor.http.cio.websocket.readText
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.websocket.webSocket
|
||||
import io.lettuce.core.Range
|
||||
import io.lettuce.core.api.async.RedisAsyncCommands
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.consumeAsFlow
|
||||
import kotlinx.coroutines.flow.mapNotNull
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@ExperimentalCoroutinesApi
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.notificationArticle(redis: RedisAsyncCommands<String, String>, client: HttpClient) {
|
||||
webSocket("/notifications") {
|
||||
val citizenId = call.citizen.id
|
||||
|
||||
launch {
|
||||
incoming.consumeAsFlow().mapNotNull { it as? Frame.Text }.collect {
|
||||
val notificationMessage = it.readText().deserialize<Event>() ?: error("unable to deserialize message")
|
||||
|
||||
redis.zremrangebyscore(
|
||||
"notification:$citizenId",
|
||||
Range.from(
|
||||
Range.Boundary.including(notificationMessage.id),
|
||||
Range.Boundary.including(notificationMessage.id)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
var score = 0.0
|
||||
while (!outgoing.isClosedForSend) {
|
||||
val result = redis.zrangebyscoreWithScores(
|
||||
"notification:$citizenId",
|
||||
Range.from(
|
||||
Range.Boundary.excluding(score),
|
||||
Range.Boundary.including(Double.POSITIVE_INFINITY)
|
||||
)
|
||||
)
|
||||
|
||||
result.get().forEach {
|
||||
outgoing.send(Frame.Text(it.value))
|
||||
if (it.score > score) score = it.score
|
||||
}
|
||||
delay(1000)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,18 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.utils.readResource
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.ContentType
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.response.respondText
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.routing.get
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
@KtorExperimentalAPI
|
||||
fun Route.definition() {
|
||||
get("/") {
|
||||
call.respondText("/openapi.yaml".readResource(), ContentType("text", "yaml"))
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.OpinionChoiceRef
|
||||
import fr.dcproject.security.voter.OpinionVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.OpinionVoter.Action.VIEW
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.dcproject.utils.toUUID
|
||||
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.put
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import io.ktor.util.KtorExperimentalAPI
|
||||
import org.koin.core.KoinComponent
|
||||
import org.koin.core.get
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
import fr.dcproject.entity.Citizen as CitizenEntity
|
||||
import fr.dcproject.repository.OpinionArticle as OpinionArticleRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object OpinionArticlePaths {
|
||||
/**
|
||||
* Get paginated opinions of citizen for all articles
|
||||
*/
|
||||
@Location("/citizens/{citizen}/opinions/articles")
|
||||
class CitizenOpinionArticleRequest(
|
||||
val citizen: CitizenRef,
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit)
|
||||
|
||||
/**
|
||||
* Put an opinion on one article
|
||||
*/
|
||||
@Location("/articles/{article}/opinions")
|
||||
@KtorExperimentalAPI
|
||||
class ArticleOpinion(val article: ArticleEntity) {
|
||||
class Body(ids: List<String>) {
|
||||
val ids = ids.map { OpinionChoiceRef(it.toUUID()) }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Opinion of citizen on targets by target ids
|
||||
*/
|
||||
@Location("/citizens/{citizen}/opinions")
|
||||
class CitizenOpinions(val citizen: CitizenEntity, id: List<String>) : KoinComponent {
|
||||
val id: List<UUID> = id.toUUID()
|
||||
val opinionsEntities = get<OpinionArticleRepository>()
|
||||
.findCitizenOpinionsByTargets(citizen, this.id)
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalAPI
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.opinionArticle(repo: OpinionArticleRepository) {
|
||||
get<OpinionArticlePaths.CitizenOpinionArticleRequest> {
|
||||
val opinions = repo.findCitizenOpinions(citizen, it.page, it.limit)
|
||||
call.respond(opinions)
|
||||
}
|
||||
|
||||
get<OpinionArticlePaths.CitizenOpinions> {
|
||||
assertCan(VIEW, it.opinionsEntities)
|
||||
|
||||
call.respond(it.opinionsEntities)
|
||||
}
|
||||
|
||||
put<OpinionArticlePaths.ArticleOpinion> {
|
||||
call.receive<OpinionArticlePaths.ArticleOpinion.Body>().ids.let { choices ->
|
||||
assertCan(CREATE, it.article)
|
||||
repo.updateOpinions(choices, citizen, it.article)
|
||||
}.let {
|
||||
call.respond(HttpStatusCode.Created, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.entity.OpinionChoice
|
||||
import fr.dcproject.security.voter.OpinionChoiceVoter.Action.VIEW
|
||||
import fr.ktorVoter.assertCan
|
||||
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
|
||||
import fr.dcproject.repository.OpinionChoice as OpinionChoiceRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object OpinionChoicePaths {
|
||||
@Location("/opinions/{opinionChoice}")
|
||||
class OpinionChoiceRequest(val opinionChoice: OpinionChoice)
|
||||
|
||||
@Location("/opinions")
|
||||
class OpinionChoicesRequest(val targets: List<String> = emptyList())
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.opinionChoice(repo: OpinionChoiceRepository) {
|
||||
get<OpinionChoicePaths.OpinionChoiceRequest> {
|
||||
assertCan(VIEW, it.opinionChoice)
|
||||
|
||||
call.respond(it.opinionChoice)
|
||||
}
|
||||
|
||||
get<OpinionChoicePaths.OpinionChoicesRequest> {
|
||||
val opinions = repo.findOpinionsChoices(it.targets)
|
||||
assertCan(VIEW, opinions)
|
||||
|
||||
call.respond(opinions)
|
||||
}
|
||||
}
|
||||
@@ -1,14 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
interface PaginatedRequestI {
|
||||
val page: Int
|
||||
val limit: Int
|
||||
}
|
||||
|
||||
open class PaginatedRequest(
|
||||
page: Int = 1,
|
||||
limit: Int = 50
|
||||
) : PaginatedRequestI {
|
||||
override val page: Int = if (page < 1) 1 else page
|
||||
override val limit: Int = if (limit > 50) 50 else if (limit < 1) 1 else limit
|
||||
}
|
||||
@@ -1,94 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.repository.CommentGeneric
|
||||
import fr.dcproject.repository.VoteComment
|
||||
import fr.dcproject.routes.VoteArticlePaths.ArticleVoteRequest
|
||||
import fr.dcproject.routes.VoteArticlePaths.CommentVoteRequest
|
||||
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
|
||||
import fr.dcproject.security.voter.VoteVoter.Action.VIEW
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.dcproject.utils.toUUID
|
||||
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.put
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import java.util.*
|
||||
import fr.dcproject.entity.Article as ArticleEntity
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
import fr.dcproject.repository.VoteArticle as VoteArticleRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object VoteArticlePaths {
|
||||
@Location("/articles/{article}/vote")
|
||||
class ArticleVoteRequest(val article: ArticleEntity) {
|
||||
data class Content(var note: Int)
|
||||
}
|
||||
|
||||
@Location("/comments/{comment}/vote")
|
||||
class CommentVoteRequest(val comment: UUID) {
|
||||
data class Content(var note: Int)
|
||||
}
|
||||
|
||||
@Location("/citizens/{citizen}/votes/articles")
|
||||
class CitizenVoteArticleRequest(
|
||||
val citizen: Citizen,
|
||||
page: Int = 1,
|
||||
limit: Int = 50,
|
||||
val search: String? = null
|
||||
) : PaginatedRequestI by PaginatedRequest(page, limit)
|
||||
|
||||
@Location("/citizens/{citizen}/votes")
|
||||
class CitizenVotesByIdsRequest(val citizen: Citizen, id: List<String>) {
|
||||
val id: List<UUID> = id.toUUID()
|
||||
}
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.voteArticle(repo: VoteArticleRepository, voteCommentRepo: VoteComment, commentRepo: CommentGeneric) {
|
||||
put<ArticleVoteRequest> {
|
||||
val content = call.receive<ArticleVoteRequest.Content>()
|
||||
val vote = VoteEntity(
|
||||
target = it.article,
|
||||
note = content.note,
|
||||
createdBy = this.citizen
|
||||
)
|
||||
assertCan(CREATE, vote)
|
||||
val votes = repo.vote(vote)
|
||||
call.respond(HttpStatusCode.Created, votes)
|
||||
}
|
||||
|
||||
put<CommentVoteRequest> {
|
||||
val comment = commentRepo.findById(it.comment)!!
|
||||
val content = call.receive<CommentVoteRequest.Content>()
|
||||
val vote = VoteEntity(
|
||||
target = comment,
|
||||
note = content.note,
|
||||
createdBy = this.citizen
|
||||
)
|
||||
assertCan(CREATE, vote)
|
||||
val votes = voteCommentRepo.vote(vote)
|
||||
call.respond(HttpStatusCode.Created, votes)
|
||||
}
|
||||
|
||||
get<VoteArticlePaths.CitizenVoteArticleRequest> {
|
||||
val votes = repo.findByCitizen(it.citizen, it.page, it.limit)
|
||||
assertCan(VIEW, votes.result)
|
||||
|
||||
call.respond(votes)
|
||||
}
|
||||
|
||||
get<VoteArticlePaths.CitizenVotesByIdsRequest> {
|
||||
val votes = repo.findCitizenVotesByTargets(it.citizen, it.id)
|
||||
if (votes.isNotEmpty()) {
|
||||
assertCan(VIEW, votes)
|
||||
}
|
||||
call.respond(votes)
|
||||
}
|
||||
}
|
||||
@@ -1,44 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.Citizen
|
||||
import fr.dcproject.routes.VoteConstitutionPaths.ConstitutionVoteRequest.Content
|
||||
import fr.dcproject.security.voter.VoteVoter.Action.CREATE
|
||||
import fr.ktorVoter.assertCan
|
||||
import io.ktor.application.call
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.locations.Location
|
||||
import io.ktor.locations.put
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
import fr.dcproject.entity.Constitution as ConstitutionEntity
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
import fr.dcproject.repository.VoteConstitution as VoteConstitutionRepository
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object VoteConstitutionPaths {
|
||||
@Location("/constitutions/{constitution}/vote")
|
||||
class ConstitutionVoteRequest(val constitution: ConstitutionEntity) {
|
||||
data class Content(var note: Int)
|
||||
}
|
||||
|
||||
@Location("/citizens/{citizen}/votes/constitutions")
|
||||
class CitizenVoteConstitutionRequest(val citizen: Citizen)
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
fun Route.voteConstitution(repo: VoteConstitutionRepository) {
|
||||
put<VoteConstitutionPaths.ConstitutionVoteRequest> {
|
||||
val content = call.receive<Content>()
|
||||
val vote = VoteEntity(
|
||||
target = it.constitution,
|
||||
note = content.note,
|
||||
createdBy = this.citizen
|
||||
)
|
||||
assertCan(CREATE, vote)
|
||||
repo.vote(vote)
|
||||
call.respond(HttpStatusCode.Created)
|
||||
}
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
package fr.dcproject.routes
|
||||
|
||||
import fr.dcproject.citizen
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.WorkgroupSimple
|
||||
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.WorkgroupVoter.Action.UPDATE
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.ADD as ADD_MEMBERS
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.UPDATE as UPDATE_MEMBERS
|
||||
import fr.dcproject.security.voter.WorkgroupVoter.ActionMembers.REMOVE as REMOVE_MEMBERS
|
||||
import fr.ktorVoter.assertCan
|
||||
import fr.dcproject.utils.toUUID
|
||||
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.locations.put
|
||||
import io.ktor.locations.delete
|
||||
import io.ktor.request.receive
|
||||
import io.ktor.response.respond
|
||||
import io.ktor.routing.Route
|
||||
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")
|
||||
open class PostWorkgroupRequest {
|
||||
class Body(
|
||||
val id: UUID?,
|
||||
val name: String,
|
||||
val description: String,
|
||||
val logo: String?,
|
||||
val anonymous: Boolean?,
|
||||
val owner: UUID?
|
||||
)
|
||||
|
||||
suspend fun getNewWorkgroup(call: ApplicationCall): WorkgroupSimple<CitizenRef> = call.receive<Body>().run {
|
||||
WorkgroupSimple(
|
||||
id ?: UUID.randomUUID(),
|
||||
name,
|
||||
description,
|
||||
logo,
|
||||
anonymous ?: true,
|
||||
owner?.let { CitizenRef(it) } ?: call.citizen,
|
||||
call.citizen
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Location("/workgroups/{workgroup}")
|
||||
class PutWorkgroupRequest(val workgroup: WorkgroupEntity) {
|
||||
class Body(
|
||||
val name: String?,
|
||||
val description: String?,
|
||||
val logo: String?,
|
||||
val anonymous: Boolean?,
|
||||
val owner: UUID?
|
||||
)
|
||||
|
||||
suspend fun updateWorkgroup(call: ApplicationCall): Unit = call.receive<Body>().run {
|
||||
name?.let { workgroup.name = it }
|
||||
description?.let { workgroup.description = it }
|
||||
logo?.let { workgroup.logo = it }
|
||||
anonymous?.let { workgroup.anonymous = it }
|
||||
}
|
||||
}
|
||||
|
||||
@Location("/workgroups/{workgroup}")
|
||||
class DeleteWorkgroupRequest(val workgroup: WorkgroupEntity)
|
||||
}
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
object WorkgroupsMembersPaths {
|
||||
@Location("/workgroups/{workgroup}/members")
|
||||
class WorkgroupsMembersRequest(val workgroup: WorkgroupEntity) {
|
||||
class Body : MutableList<Body.Item> by mutableListOf() {
|
||||
class Item(id: String) {
|
||||
val id = id.toUUID()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getMembers(call: ApplicationCall): List<CitizenRef> = call.receive<Body>().map {
|
||||
CitizenRef(it.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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> {
|
||||
it.getNewWorkgroup(call)
|
||||
.let { workgroup ->
|
||||
assertCan(CREATE, workgroup)
|
||||
repo.upsert(workgroup)
|
||||
}.let {
|
||||
call.respond(HttpStatusCode.Created, it)
|
||||
}
|
||||
}
|
||||
|
||||
put<WorkgroupsPaths.PutWorkgroupRequest> {
|
||||
it.updateWorkgroup(call).let { workgroup ->
|
||||
assertCan(UPDATE, workgroup)
|
||||
repo.upsert(workgroup as WorkgroupSimple<CitizenRef>)
|
||||
}.let {
|
||||
call.respond(HttpStatusCode.OK, it)
|
||||
}
|
||||
}
|
||||
|
||||
delete<WorkgroupsPaths.DeleteWorkgroupRequest> {
|
||||
assertCan(UPDATE, it.workgroup)
|
||||
repo.delete(it.workgroup)
|
||||
call.respond(HttpStatusCode.NoContent, it)
|
||||
}
|
||||
|
||||
/* Add members to workgroup */
|
||||
post<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(ADD_MEMBERS, it.workgroup)
|
||||
repo.addMembers(it.workgroup, members)
|
||||
}.let {
|
||||
call.respond(HttpStatusCode.Created, it)
|
||||
}
|
||||
}
|
||||
|
||||
/* Delete members of workgroup */
|
||||
delete<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(REMOVE_MEMBERS, it.workgroup)
|
||||
repo.removeMembers(it.workgroup, members)
|
||||
}.let {
|
||||
call.respond(HttpStatusCode.OK, it)
|
||||
}
|
||||
}
|
||||
|
||||
/* Update members of workgroup */
|
||||
put<WorkgroupsMembersPaths.WorkgroupsMembersRequest> {
|
||||
it.getMembers(call)
|
||||
.let { members ->
|
||||
assertCan(UPDATE_MEMBERS, it.workgroup)
|
||||
repo.updateMembers(it.workgroup, members)
|
||||
}.let {
|
||||
call.respond(HttpStatusCode.OK, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.ArticleAuthI
|
||||
import fr.dcproject.entity.ArticleI
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import fr.ktorVoter.checkClass
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.entity.Comment as CommentEntity
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
|
||||
class ArticleVoter : Voter {
|
||||
enum class Action : ActionI {
|
||||
CREATE,
|
||||
UPDATE,
|
||||
VIEW,
|
||||
DELETE
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return (action is Action || action is CommentVoter.Action || action is VoteVoter.Action)
|
||||
.and(subject is ArticleI? || subject is VoteEntity<*> || subject is CommentEntity<*>)
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
val user = call.user
|
||||
if (action == Action.CREATE && user is UserI) return Vote.GRANTED
|
||||
if (action == Action.VIEW) return view(subject, user)
|
||||
if (action == Action.DELETE) return delete(subject, user)
|
||||
if (action == Action.UPDATE) return update(subject, user)
|
||||
if (action is CommentVoter.Action) return voteForComment(action, subject)
|
||||
if (action is VoteVoter.Action) return voteForVote(action, subject)
|
||||
if (action is Action) return Vote.DENIED
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
|
||||
private fun view(subject: Any?, user: UserI?): Vote {
|
||||
checkClass(ArticleAuthI::class, subject)
|
||||
if (subject is ArticleAuthI<*>) {
|
||||
return if (subject.isDeleted()) Vote.DENIED
|
||||
else if (subject.draft && (user == null || subject.createdBy.user.id != user.id)) Vote.DENIED
|
||||
else Vote.GRANTED
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
private fun delete(subject: Any?, user: UserI?): Vote {
|
||||
checkClass(ArticleAuthI::class, subject)
|
||||
if (subject is ArticleAuthI<*>) {
|
||||
if (user is UserI && subject.createdBy.user.id == user.id) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
private fun update(subject: Any?, user: UserI?): Vote {
|
||||
checkClass(ArticleAuthI::class, subject)
|
||||
if (subject is ArticleAuthI<*>) {
|
||||
if (user is UserI && subject.createdBy.user.id == user.id) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
private fun voteForVote(action: VoteVoter.Action, subject: Any?): Vote {
|
||||
if (action == VoteVoter.Action.CREATE && subject is VoteEntity<*>) {
|
||||
val target = subject.target
|
||||
if (target is ArticleAuthI<*>) {
|
||||
if (target.isDeleted()) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
} else if (target is ArticleI) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
}
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
|
||||
private fun voteForComment(action: CommentVoter.Action, subject: Any?): Vote {
|
||||
if (subject is CommentEntity<*>) {
|
||||
val target = subject.target
|
||||
if (target is ArticleAuthI<*>) {
|
||||
if (target.isDeleted()) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
} else if (target is ArticleI) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
if (action == CommentVoter.Action.CREATE) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == CommentVoter.Action.VIEW) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
} else {
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.CitizenBasicI
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import io.ktor.locations.KtorExperimentalLocationsAPI
|
||||
|
||||
@KtorExperimentalLocationsAPI
|
||||
class CitizenVoter : Voter {
|
||||
enum class Action : ActionI {
|
||||
CREATE,
|
||||
UPDATE,
|
||||
VIEW,
|
||||
DELETE,
|
||||
CHANGE_PASSWORD
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return (action is Action)
|
||||
.and(subject is CitizenBasicI?)
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
val user = call.user
|
||||
if (action == Action.CREATE && user != null) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (user == null) return Vote.DENIED
|
||||
if (subject is CitizenBasicI) {
|
||||
return if (subject.isDeleted()) Vote.DENIED
|
||||
else Vote.GRANTED
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
if (action == Action.DELETE) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
if (action == Action.UPDATE &&
|
||||
user is UserI &&
|
||||
subject is CitizenBasicI &&
|
||||
subject.user.id == user.id
|
||||
) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.CHANGE_PASSWORD && user != null && subject is CitizenBasicI) {
|
||||
val userToChange = subject.user
|
||||
return if (user.id == userToChange.id) {
|
||||
Vote.GRANTED
|
||||
} else {
|
||||
Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
|
||||
if (action is Action) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
@@ -1,59 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.Comment
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
|
||||
class CommentVoter : 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 Comment<*>?)
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
val user = call.user
|
||||
|
||||
if (subject !is Comment<*>) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
if (action == Action.CREATE) {
|
||||
if (user == null) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
if (subject.createdBy.user.id != user.id) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
return if (subject.isDeleted()) Vote.DENIED
|
||||
else Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.UPDATE && user != null && user.id == subject.createdBy.user.id) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.DELETE) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
if (action is Action) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.Comment
|
||||
import fr.dcproject.entity.ConstitutionSimple
|
||||
import fr.dcproject.entity.UserI
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
|
||||
class ConstitutionVoter : Voter {
|
||||
enum class Action : ActionI {
|
||||
CREATE,
|
||||
UPDATE,
|
||||
VIEW,
|
||||
DELETE
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return (action is Action || action is CommentVoter.Action || action is VoteVoter.Action)
|
||||
.and(subject is ConstitutionSimple<*, *>? || subject is VoteEntity<*> || subject is Comment<*>)
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
val user = call.user
|
||||
if (action == Action.CREATE && user != null) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is ConstitutionSimple<*, *>) {
|
||||
return if (subject.isDeleted()) Vote.DENIED
|
||||
else Vote.GRANTED
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
if (action == Action.DELETE && user is UserI && subject is ConstitutionSimple<*, *> && subject.createdBy.user.id == user.id) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.UPDATE && user is UserI && subject is ConstitutionSimple<*, *> && subject.createdBy.user.id == user.id) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action is CommentVoter.Action) return voteForComment(action)
|
||||
if (action is VoteVoter.Action) return voteForVote(action, subject)
|
||||
|
||||
if (action is Action) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
|
||||
private fun voteForVote(action: VoteVoter.Action, subject: Any?): Vote {
|
||||
if (action == VoteVoter.Action.CREATE && subject is VoteEntity<*>) {
|
||||
val target = subject.target
|
||||
if (target !is ConstitutionSimple<*, *>) {
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
if (target.isDeleted()) {
|
||||
return Vote.DENIED
|
||||
}
|
||||
}
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
|
||||
private fun voteForComment(action: CommentVoter.Action): Vote {
|
||||
if (action == CommentVoter.Action.CREATE) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == CommentVoter.Action.VIEW) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.entity.Follow as FollowEntity
|
||||
import fr.dcproject.entity.User as UserEntity
|
||||
|
||||
class FollowVoter : Voter {
|
||||
enum class Action : ActionI {
|
||||
CREATE,
|
||||
DELETE,
|
||||
VIEW
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return (action is Action)
|
||||
.and(subject is FollowEntity<*>?)
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
val user = call.user
|
||||
if (action == Action.CREATE) {
|
||||
return if (user != null) Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
}
|
||||
|
||||
if (action == Action.DELETE) {
|
||||
return if (user != null) Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is FollowEntity<*>) {
|
||||
return voteView(user, subject)
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
|
||||
private fun voteView(user: UserEntity?, subject: FollowEntity<*>): Vote {
|
||||
return if ((user != null && subject.createdBy.user.id == user.id) || !subject.createdBy.followAnonymous) Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.OpinionChoice
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
|
||||
class OpinionChoiceVoter : Voter {
|
||||
enum class Action : ActionI {
|
||||
VIEW
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return (action is Action)
|
||||
.and(subject is OpinionChoice?)
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is OpinionChoice) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.entity.ArticleAuthI
|
||||
import fr.dcproject.entity.Opinion
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
|
||||
class OpinionVoter : Voter {
|
||||
enum class Action : ActionI {
|
||||
CREATE,
|
||||
VIEW,
|
||||
DELETE
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return (action is Action)
|
||||
.and(subject is Opinion<*>? || subject is ArticleAuthI<*>)
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
val user = call.user
|
||||
if (action == Action.CREATE) {
|
||||
return if (user != null && (
|
||||
(subject is ArticleAuthI<*> && !subject.isDeleted()) ||
|
||||
(subject is Opinion<*> && subject.createdBy.user.id == user.id)
|
||||
)) Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
}
|
||||
|
||||
if (action == Action.VIEW) {
|
||||
if (subject is Opinion<*>) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
if (action == Action.DELETE) {
|
||||
return if (subject is Opinion<*> &&
|
||||
user != null &&
|
||||
subject.createdBy.user.id == user.id
|
||||
)
|
||||
Vote.GRANTED
|
||||
else Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import io.ktor.application.ApplicationCall
|
||||
import fr.dcproject.entity.Vote as VoteEntity
|
||||
|
||||
class VoteVoter : Voter {
|
||||
enum class Action : ActionI {
|
||||
CREATE,
|
||||
VIEW
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return action is Action && subject is VoteEntity<*>?
|
||||
}
|
||||
|
||||
override fun vote(action: ActionI, call: ApplicationCall, subject: Any?): Vote {
|
||||
val user = call.user
|
||||
if (action == Action.CREATE && user != null) {
|
||||
return Vote.GRANTED
|
||||
}
|
||||
|
||||
if (action == Action.VIEW && user != null) {
|
||||
if (subject is VoteEntity<*>) {
|
||||
return if (subject.createdBy.user.id != user.id) {
|
||||
Vote.DENIED
|
||||
} else {
|
||||
Vote.GRANTED
|
||||
}
|
||||
}
|
||||
return Vote.DENIED
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
package fr.dcproject.security.voter
|
||||
|
||||
import fr.dcproject.citizenOrNull
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.user
|
||||
import fr.ktorVoter.ActionI
|
||||
import fr.ktorVoter.Vote
|
||||
import fr.ktorVoter.Voter
|
||||
import fr.ktorVoter.VoterException
|
||||
import io.ktor.application.ApplicationCall
|
||||
|
||||
class WorkgroupVoter : Voter {
|
||||
enum class Action : ActionI {
|
||||
CREATE,
|
||||
UPDATE,
|
||||
VIEW,
|
||||
DELETE,
|
||||
}
|
||||
|
||||
enum class ActionMembers : ActionI {
|
||||
ADD,
|
||||
UPDATE,
|
||||
VIEW,
|
||||
REMOVE,
|
||||
}
|
||||
|
||||
override fun supports(action: ActionI, call: ApplicationCall, subject: Any?): Boolean {
|
||||
return (action is Action || action is ActionMembers)
|
||||
.and(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
|
||||
}
|
||||
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
|
||||
} else if (subject !is WorkgroupWithAuthI<*> && (action == Action.DELETE || action == Action.UPDATE)) {
|
||||
throw object :
|
||||
VoterException("Unable to define if your are granted, the subject must implement 'WorkgroupWithAuthI'") {}
|
||||
}
|
||||
|
||||
if (action == ActionMembers.ADD) {
|
||||
val citizen = call.citizenOrNull
|
||||
// TODO create ROLES
|
||||
return Vote.isGranted {
|
||||
citizen != null &&
|
||||
subject is WorkgroupWithMembersI<*> &&
|
||||
subject.members.asCitizen(citizen)
|
||||
}
|
||||
}
|
||||
|
||||
if (action == ActionMembers.UPDATE) {
|
||||
val citizen = call.citizenOrNull
|
||||
// TODO create ROLES
|
||||
return Vote.isGranted {
|
||||
citizen != null &&
|
||||
subject is WorkgroupWithMembersI<*> &&
|
||||
subject.members.asCitizen(citizen)
|
||||
}
|
||||
}
|
||||
|
||||
if (action == ActionMembers.REMOVE) {
|
||||
val citizen = call.citizenOrNull
|
||||
// TODO create ROLES
|
||||
return Vote.isGranted {
|
||||
citizen != null &&
|
||||
subject is WorkgroupWithMembersI<*> &&
|
||||
subject.members.asCitizen(citizen)
|
||||
}
|
||||
}
|
||||
|
||||
return Vote.ABSTAIN
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
package fr.dcproject.utils
|
||||
|
||||
import org.joda.time.DateTime
|
||||
import org.joda.time.format.ISODateTimeFormat
|
||||
|
||||
fun DateTime.toIso() = ISODateTimeFormat.dateTime().print(this)
|
||||
@@ -1,29 +0,0 @@
|
||||
package fr.dcproject.utils
|
||||
|
||||
import com.jayway.jsonpath.JsonPath
|
||||
import com.jayway.jsonpath.PathNotFoundException
|
||||
import org.apache.http.util.EntityUtils
|
||||
import org.elasticsearch.client.Response
|
||||
import org.slf4j.LoggerFactory
|
||||
|
||||
fun Response.contentToString(): String {
|
||||
return EntityUtils.toString(this.entity)
|
||||
}
|
||||
|
||||
fun Response.getField(jsonPath: String): Int? {
|
||||
return try {
|
||||
JsonPath.read(this.contentToString(), jsonPath)
|
||||
} catch (e: PathNotFoundException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
fun String.getJsonField(jsonPath: String): Int? {
|
||||
return try {
|
||||
JsonPath.read(this, jsonPath)
|
||||
} catch (e: PathNotFoundException) {
|
||||
LoggerFactory.getLogger("fr.dcproject.utils.getJsonField")
|
||||
.warn("No value for Json path ${JsonPath.compile(jsonPath).path}")
|
||||
null
|
||||
}
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package fr.dcproject.utils
|
||||
|
||||
import org.slf4j.Logger
|
||||
import org.slf4j.LoggerFactory
|
||||
import kotlin.properties.ReadOnlyProperty
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
internal class LoggerDelegate<in R : Any> : ReadOnlyProperty<R, Logger> {
|
||||
override fun getValue(thisRef: R, property: KProperty<*>) = LoggerFactory.getLogger(thisRef.javaClass.packageName)
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
package fr.dcproject.utils
|
||||
|
||||
fun String.readResource(callbak: (String) -> Unit = {}): String {
|
||||
val content = callbak::class.java.getResource(this).readText()
|
||||
callbak(content)
|
||||
return content
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
package fr.dcproject.utils
|
||||
|
||||
import java.util.*
|
||||
|
||||
fun String.toUUID(): UUID = UUID.fromString(this.trim())
|
||||
|
||||
fun List<String>.toUUID(): List<UUID> = this
|
||||
.map { it.trim() }
|
||||
.filter { it.isNotBlank() }
|
||||
.map { UUID.fromString(it) }
|
||||
@@ -1,84 +0,0 @@
|
||||
package fr.dcproject.views
|
||||
|
||||
import fr.dcproject.entity.*
|
||||
import fr.dcproject.utils.contentToString
|
||||
import fr.dcproject.utils.getJsonField
|
||||
import fr.dcproject.utils.toIso
|
||||
import org.elasticsearch.client.Request
|
||||
import org.elasticsearch.client.Response
|
||||
import org.elasticsearch.client.RestClient
|
||||
import org.joda.time.DateTime
|
||||
import java.util.*
|
||||
|
||||
class ArticleViewManager(private val restClient: RestClient) : ViewManager<ArticleRefVersioning> {
|
||||
override fun addView(ip: String, article: ArticleRefVersioning, citizen: CitizenRef?, dateTime: DateTime): Response? {
|
||||
val isLogged = (citizen != null).toString()
|
||||
val ref = citizen?.id ?: UUID.nameUUIDFromBytes(ip.toByteArray())!!
|
||||
val request = Request(
|
||||
"POST",
|
||||
"/views/_doc/"
|
||||
).apply {
|
||||
//language=JSON
|
||||
setJsonEntity("""
|
||||
{
|
||||
"logged": $isLogged,
|
||||
"type": "article",
|
||||
"user_ref": "$ref",
|
||||
"ip": "$ip",
|
||||
"id": "${article.id}",
|
||||
"version_id": "${article.versionId}",
|
||||
"citizen_id": "${citizen?.id}",
|
||||
"view_at": "${dateTime.toIso()}"
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
return restClient.performRequest(request)
|
||||
}
|
||||
|
||||
override fun getViewsCount(article: ArticleRefVersioning): ViewAggregation {
|
||||
val request = Request(
|
||||
"GET",
|
||||
"/views/_search"
|
||||
).apply {
|
||||
//language=JSON
|
||||
setJsonEntity("""
|
||||
{
|
||||
"size": 0,
|
||||
"query": {
|
||||
"bool": {
|
||||
"must": {
|
||||
"term": {
|
||||
"version_id": "${article.versionId}"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"aggs" : {
|
||||
"total": {
|
||||
"composite" : {
|
||||
"sources" : [
|
||||
{ "version_id": { "terms": {"field": "version_id" } } }
|
||||
]
|
||||
}
|
||||
},
|
||||
"unique" : {
|
||||
"cardinality" : {
|
||||
"field" : "user_ref",
|
||||
"precision_threshold": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
""".trimIndent())
|
||||
}
|
||||
|
||||
return restClient
|
||||
.performRequest(request).contentToString().run {
|
||||
ViewAggregation(
|
||||
getJsonField("$.aggregations.total.buckets[0].doc_count") ?: 0,
|
||||
getJsonField("$.aggregations.unique.value") ?: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
package fr.dcproject.views
|
||||
|
||||
import fr.dcproject.entity.CitizenRef
|
||||
import fr.dcproject.entity.ViewAggregation
|
||||
import org.elasticsearch.client.Response
|
||||
import org.joda.time.DateTime
|
||||
|
||||
interface ViewManager <T> {
|
||||
fun addView(ip: String, entity: T, citizen: CitizenRef? = null, dateTime: DateTime = DateTime.now()): Response?
|
||||
fun getViewsCount(entity: T): ViewAggregation
|
||||
}
|
||||
Reference in New Issue
Block a user