Skip to content

Commit

Permalink
feat: project page + setup admin
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfallet committed Apr 11, 2024
1 parent 7c8c8da commit 1cd5b89
Show file tree
Hide file tree
Showing 37 changed files with 1,205 additions and 10 deletions.
2 changes: 2 additions & 0 deletions groupeminaste-backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ kotlin {
implementation("me.nathanfallet.ktorx:ktor-i18n-freemarker:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-routers:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-routers-locale:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-routers-admin:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-routers-admin-locale:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-sentry:$ktorxVersion")
implementation("me.nathanfallet.cloudflare:cloudflare-api-client:4.2.3")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ fun Application.module() {
configureSessions()
configureTemplating()
configureRouting()
configureStatusPage()
configureMonitoring()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package me.nathanfallet.groupeminaste.controllers.dashboard

class DashboardController : IDashboardController {

override suspend fun dashboard() {}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package me.nathanfallet.groupeminaste.controllers.dashboard

import me.nathanfallet.groupeminaste.controllers.models.AdminUnitRouter
import me.nathanfallet.groupeminaste.usecases.web.IGetAdminMenuForCallUseCase
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase
import me.nathanfallet.usecases.localization.ITranslateUseCase

class DashboardRouter(
controller: IDashboardController,
getLocaleForCallUseCase: IGetLocaleForCallUseCase,
translateUseCase: ITranslateUseCase,
getAdminMenuForCallUseCase: IGetAdminMenuForCallUseCase,
) : AdminUnitRouter(
controller,
IDashboardController::class,
getLocaleForCallUseCase,
translateUseCase,
getAdminMenuForCallUseCase
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package me.nathanfallet.groupeminaste.controllers.dashboard

import me.nathanfallet.ktorx.controllers.IUnitController
import me.nathanfallet.ktorx.models.annotations.AdminTemplateMapping
import me.nathanfallet.ktorx.models.annotations.Path

interface IDashboardController : IUnitController {

@AdminTemplateMapping("admin/dashboard.ftl")
@Path("GET", "/")
suspend fun dashboard()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package me.nathanfallet.groupeminaste.controllers.models

import io.ktor.server.freemarker.*
import io.ktor.util.reflect.*
import me.nathanfallet.groupeminaste.usecases.web.IGetAdminMenuForCallUseCase
import me.nathanfallet.ktorx.controllers.IChildModelController
import me.nathanfallet.ktorx.routers.IChildModelRouter
import me.nathanfallet.ktorx.routers.admin.LocalizedAdminChildModelRouter
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase
import me.nathanfallet.usecases.localization.ITranslateUseCase
import me.nathanfallet.usecases.models.IChildModel
import me.nathanfallet.usecases.models.annotations.PayloadKey
import kotlin.reflect.KClass

open class AdminChildModelRouter<Model : IChildModel<Id, CreatePayload, UpdatePayload, ParentId>, Id, CreatePayload : Any, UpdatePayload : Any, ParentModel : IChildModel<ParentId, *, *, *>, ParentId>(
modelTypeInfo: TypeInfo,
createPayloadTypeInfo: TypeInfo,
updatePayloadTypeInfo: TypeInfo,
controller: IChildModelController<Model, Id, CreatePayload, UpdatePayload, ParentModel, ParentId>,
controllerClass: KClass<out IChildModelController<Model, Id, CreatePayload, UpdatePayload, ParentModel, ParentId>>,
parentRouter: IChildModelRouter<ParentModel, ParentId, *, *, *, *>?,
getLocaleForCallUseCase: IGetLocaleForCallUseCase,
translateUseCase: ITranslateUseCase,
getAdminMenuForCallUseCase: IGetAdminMenuForCallUseCase,
route: String? = null,
id: String? = null,
) : LocalizedAdminChildModelRouter<Model, Id, CreatePayload, UpdatePayload, ParentModel, ParentId>(
modelTypeInfo,
createPayloadTypeInfo,
updatePayloadTypeInfo,
controller,
controllerClass,
parentRouter,
{ template, model ->
if (template == "root/error.ftl") respondTemplate(template, model)
else respondTemplate(
template, model + mapOf(
"title" to translateUseCase(
getLocaleForCallUseCase(this),
((model["route"] as String).takeIf { it.isNotEmpty() } ?: "dashboard").let { "admin_menu_$it" }
),
"menu" to getAdminMenuForCallUseCase(this),
"flatpickr" to ((model["keys"] as? List<*>)?.any { (it as? PayloadKey)?.type == "date" } == true),
)
)
},
getLocaleForCallUseCase,
"root/error.ftl",
"/auth/login?redirect={path}",
"admin/models/list.ftl",
null,
"admin/models/form.ftl",
"admin/models/form.ftl",
"admin/models/delete.ftl",
route,
id,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package me.nathanfallet.groupeminaste.controllers.models

import io.ktor.util.reflect.*
import me.nathanfallet.groupeminaste.usecases.web.IGetAdminMenuForCallUseCase
import me.nathanfallet.ktorx.controllers.IModelController
import me.nathanfallet.ktorx.routers.IModelRouter
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase
import me.nathanfallet.usecases.localization.ITranslateUseCase
import me.nathanfallet.usecases.models.IModel
import me.nathanfallet.usecases.models.UnitModel
import kotlin.reflect.KClass

open class AdminModelRouter<Model : IModel<Id, CreatePayload, UpdatePayload>, Id, CreatePayload : Any, UpdatePayload : Any>(
modelTypeInfo: TypeInfo,
createPayloadTypeInfo: TypeInfo,
updatePayloadTypeInfo: TypeInfo,
controller: IModelController<Model, Id, CreatePayload, UpdatePayload>,
controllerClass: KClass<out IModelController<Model, Id, CreatePayload, UpdatePayload>>,
getLocaleForCallUseCase: IGetLocaleForCallUseCase,
translateUseCase: ITranslateUseCase,
getAdminMenuForCallUseCase: IGetAdminMenuForCallUseCase,
route: String? = null,
id: String? = null,
) : AdminChildModelRouter<Model, Id, CreatePayload, UpdatePayload, UnitModel, Unit>(
modelTypeInfo,
createPayloadTypeInfo,
updatePayloadTypeInfo,
controller,
controllerClass,
null,
getLocaleForCallUseCase,
translateUseCase,
getAdminMenuForCallUseCase,
route,
id,
), IModelRouter<Model, Id, CreatePayload, UpdatePayload>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package me.nathanfallet.groupeminaste.controllers.models

import io.ktor.util.reflect.*
import me.nathanfallet.groupeminaste.usecases.web.IGetAdminMenuForCallUseCase
import me.nathanfallet.ktorx.controllers.IUnitController
import me.nathanfallet.ktorx.routers.IUnitRouter
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase
import me.nathanfallet.usecases.localization.ITranslateUseCase
import me.nathanfallet.usecases.models.UnitModel
import kotlin.reflect.KClass

open class AdminUnitRouter(
controller: IUnitController,
controllerClass: KClass<out IUnitController>,
getLocaleForCallUseCase: IGetLocaleForCallUseCase,
translateUseCase: ITranslateUseCase,
getAdminMenuForCallUseCase: IGetAdminMenuForCallUseCase,
route: String? = null,
) : AdminModelRouter<UnitModel, Unit, Unit, Unit>(
typeInfo<UnitModel>(),
typeInfo<Unit>(),
typeInfo<Unit>(),
controller,
controllerClass,
getLocaleForCallUseCase,
translateUseCase,
getAdminMenuForCallUseCase,
route ?: "",
), IUnitRouter
Original file line number Diff line number Diff line change
@@ -1,21 +1,53 @@
package me.nathanfallet.groupeminaste.controllers.projects

import io.ktor.server.application.*
import me.nathanfallet.groupeminaste.models.projects.Project
import me.nathanfallet.groupeminaste.models.projects.ProjectPayload
import me.nathanfallet.ktorx.controllers.IModelController
import me.nathanfallet.ktorx.models.annotations.APIMapping
import me.nathanfallet.ktorx.models.annotations.ListModelPath
import me.nathanfallet.ktorx.models.annotations.Path
import me.nathanfallet.ktorx.models.annotations.TemplateMapping
import me.nathanfallet.ktorx.models.annotations.*

interface IProjectsController : IModelController<Project, String, ProjectPayload, ProjectPayload> {

@APIMapping
@AdminTemplateMapping
@ListModelPath
suspend fun list(): List<Project>

@TemplateMapping("public/projects/list.ftl")
@Path("GET", "/")
suspend fun listTemplate(): Map<String, Any>

@APIMapping
@GetModelPath
@DocumentedError(404, "projects_not_found")
suspend fun get(call: ApplicationCall, @Id id: String): Project

@TemplateMapping("public/projects/details.ftl")
@Path("GET", "/{projectId}")
suspend fun details(call: ApplicationCall, @Id id: String): Map<String, Any>

@APIMapping
@CreateModelPath
@AdminTemplateMapping
@DocumentedError(401, "auth_invalid_credentials")
@DocumentedError(500, "error_internal")
suspend fun create(call: ApplicationCall, @Payload payload: ProjectPayload): Project

@APIMapping
@UpdateModelPath
@AdminTemplateMapping
@DocumentedError(401, "auth_invalid_credentials")
@DocumentedError(404, "projects_not_found")
@DocumentedError(500, "error_internal")
suspend fun update(call: ApplicationCall, @Id id: String, @Payload payload: ProjectPayload): Project

@APIMapping
@DeleteModelPath
@AdminTemplateMapping
@DocumentedType(Project::class)
@DocumentedError(401, "auth_invalid_credentials")
@DocumentedError(404, "projects_not_found")
@DocumentedError(500, "error_internal")
suspend fun delete(call: ApplicationCall, @Id id: String)

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
package me.nathanfallet.groupeminaste.controllers.projects

import io.ktor.http.*
import io.ktor.server.application.*
import me.nathanfallet.groupeminaste.models.projects.Project
import me.nathanfallet.groupeminaste.models.projects.ProjectLink
import me.nathanfallet.groupeminaste.models.projects.ProjectPayload
import me.nathanfallet.ktorx.models.exceptions.ControllerException
import me.nathanfallet.ktorx.usecases.users.IRequireUserForCallUseCase
import me.nathanfallet.usecases.models.create.ICreateModelSuspendUseCase
import me.nathanfallet.usecases.models.delete.IDeleteModelSuspendUseCase
import me.nathanfallet.usecases.models.get.IGetModelSuspendUseCase
import me.nathanfallet.usecases.models.list.IListChildModelSuspendUseCase
import me.nathanfallet.usecases.models.list.IListModelSuspendUseCase
import me.nathanfallet.usecases.models.update.IUpdateModelSuspendUseCase

class ProjectsController(
private val requireUserForCallUseCase: IRequireUserForCallUseCase,
private val listProjectsUseCase: IListModelSuspendUseCase<Project>,
private val getProjectUseCase: IGetModelSuspendUseCase<Project, String>,
private val createProjectUseCase: ICreateModelSuspendUseCase<Project, ProjectPayload>,
private val updateProjectUseCase: IUpdateModelSuspendUseCase<Project, String, ProjectPayload>,
private val deleteProjectUseCase: IDeleteModelSuspendUseCase<Project, String>,
private val listProjectLinksUseCase: IListChildModelSuspendUseCase<ProjectLink, String>,
) : IProjectsController {

override suspend fun list(): List<Project> {
Expand All @@ -19,4 +36,47 @@ class ProjectsController(
)
}

override suspend fun get(call: ApplicationCall, id: String): Project {
return getProjectUseCase(id) ?: throw ControllerException(
HttpStatusCode.NotFound, "projects_not_found"
)
}

override suspend fun details(call: ApplicationCall, id: String): Map<String, Any> {
val project = getProjectUseCase(id) ?: throw ControllerException(
HttpStatusCode.NotFound, "projects_not_found"
)
return mapOf(
"item" to project,
"links" to listProjectLinksUseCase(id)
)
}

override suspend fun create(call: ApplicationCall, payload: ProjectPayload): Project {
requireUserForCallUseCase(call)
return createProjectUseCase(payload) ?: throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
}

override suspend fun update(call: ApplicationCall, id: String, payload: ProjectPayload): Project {
requireUserForCallUseCase(call)
val project = getProjectUseCase(id) ?: throw ControllerException(
HttpStatusCode.NotFound, "projects_not_found"
)
return updateProjectUseCase(project.id, payload) ?: throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
}

override suspend fun delete(call: ApplicationCall, id: String) {
requireUserForCallUseCase(call)
val project = getProjectUseCase(id) ?: throw ControllerException(
HttpStatusCode.NotFound, "projects_not_found"
)
if (!deleteProjectUseCase(project.id)) throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,21 @@ package me.nathanfallet.groupeminaste.controllers.projects

import io.ktor.server.freemarker.*
import io.ktor.util.reflect.*
import me.nathanfallet.groupeminaste.controllers.models.AdminModelRouter
import me.nathanfallet.groupeminaste.models.projects.Project
import me.nathanfallet.groupeminaste.models.projects.ProjectPayload
import me.nathanfallet.groupeminaste.usecases.web.IGetAdminMenuForCallUseCase
import me.nathanfallet.ktorx.routers.api.APIModelRouter
import me.nathanfallet.ktorx.routers.concat.ConcatModelRouter
import me.nathanfallet.ktorx.routers.templates.LocalizedTemplateModelRouter
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase
import me.nathanfallet.usecases.localization.ITranslateUseCase

class ProjectsRouter(
controller: IProjectsController,
getLocaleForCallUseCase: IGetLocaleForCallUseCase,
translateUseCase: ITranslateUseCase,
getAdminMenuForCallUseCase: IGetAdminMenuForCallUseCase,
) : ConcatModelRouter<Project, String, ProjectPayload, ProjectPayload>(
APIModelRouter(
typeInfo<Project>(),
Expand All @@ -31,5 +36,15 @@ class ProjectsRouter(
respondTemplate(template, model)
},
getLocaleForCallUseCase
),
AdminModelRouter(
typeInfo<Project>(),
typeInfo<ProjectPayload>(),
typeInfo<ProjectPayload>(),
controller,
IProjectsController::class,
getLocaleForCallUseCase,
translateUseCase,
getAdminMenuForCallUseCase
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package me.nathanfallet.groupeminaste.database.projects

import me.nathanfallet.groupeminaste.database.users.Users
import me.nathanfallet.groupeminaste.extensions.generateId
import me.nathanfallet.groupeminaste.models.projects.ProjectLink
import org.jetbrains.exposed.sql.ResultRow
import org.jetbrains.exposed.sql.Table
import org.jetbrains.exposed.sql.selectAll

object ProjectLinks : Table() {

val id = varchar("id", 32)
val projectId = varchar("project_id", 32).index()
val name = varchar("name", 255)
val url = text("url")

override val primaryKey = PrimaryKey(Users.id)

fun generateId(): String {
val candidate = String.generateId()
return if (selectAll().where { id eq candidate }.count() > 0) generateId() else candidate
}

fun toProjectLink(
row: ResultRow,
) = ProjectLink(
row[id],
row[projectId],
row[name],
row[url]
)

}
Loading

0 comments on commit 1cd5b89

Please sign in to comment.