Remove sub directories

This commit is contained in:
2020-05-12 10:07:01 +02:00
parent 678a2f48d2
commit 4504600268
77 changed files with 1 additions and 0 deletions

View File

@@ -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()
}
}

View File

@@ -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>()

View File

@@ -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)
}

View File

@@ -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()) }
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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)

View File

@@ -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())

View File

@@ -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()
}

View File

@@ -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>
}

View File

@@ -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)
}

View File

@@ -1,9 +0,0 @@
package fr.dcproject.entity
interface Viewable {
var views: ViewAggregation
}
class ViewableImp : Viewable {
override var views: ViewAggregation = ViewAggregation()
}

View File

@@ -1,9 +0,0 @@
package fr.dcproject.entity
interface Votable {
var votes: VoteAggregation
}
class VotableImp : Votable {
override var votes: VoteAggregation = VoteAggregation()
}

View File

@@ -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")
}
}
}

View File

@@ -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)
}

View File

@@ -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

View File

@@ -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)
}
}

View File

@@ -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()
}
}
}

View File

@@ -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")
}
}

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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
)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}

View File

@@ -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
)
}
}

View File

@@ -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)
}
}

View File

@@ -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
)
}

View File

@@ -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
}

View File

@@ -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))
}
}
}

View File

@@ -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()

View File

@@ -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")
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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"))
}
}

View File

@@ -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)
}
}
}

View File

@@ -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)
}
}

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}

View File

@@ -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)
}
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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)

View File

@@ -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
}
}

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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) }

View File

@@ -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
)
}
}
}

View File

@@ -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
}