From 8b8dbd6fe5408e13bd5b672f0c7e56539c5eab07 Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Sat, 24 Oct 2020 01:25:25 +0200 Subject: [PATCH] Separate views into a maven module --- pom.xml | 1 + simplenotes-app/pom.xml | 16 ++-- .../kotlin/be/simplenotes/app/SimpleNotes.kt | 7 +- .../simplenotes/app/api/ApiNoteController.kt | 22 +++--- .../app/controllers/BaseController.kt | 8 +- .../app/controllers/NoteController.kt | 74 +++++++++---------- .../app/controllers/SettingsController.kt | 28 +++---- .../app/controllers/UserController.kt | 22 +++--- .../be/simplenotes/app/filters/AuthFilter.kt | 4 +- .../be/simplenotes/app/filters/ErrorFilter.kt | 4 +- .../simplenotes/app/modules/ConfigModule.kt | 2 + .../be/simplenotes/app/modules/CoreModules.kt | 19 +---- .../simplenotes/app/modules/ServerModule.kt | 4 +- .../be/simplenotes/app/routes/Router.kt | 6 +- .../be/simplenotes/app/utils/PrettyDate.kt | 10 --- .../simplenotes/app/filters/AuthFilterTest.kt | 6 +- .../simplenotes/domain/security/JwtPayload.kt | 10 +-- .../simplenotes/domain/security/SimpleJwt.kt | 7 +- .../usecases/users/login/LoginUseCaseImpl.kt | 4 +- ...orTest.kt => LoggedInUserExtractorTest.kt} | 5 +- .../main/kotlin/be/simplenotes/types/User.kt | 3 + simplenotes-views/pom.xml | 41 ++++++++++ .../kotlin/be/simplenotes}/views/BaseView.kt | 11 ++- .../kotlin/be/simplenotes}/views/ErrorView.kt | 11 ++- .../kotlin/be/simplenotes}/views/NoteView.kt | 30 ++++---- .../be/simplenotes}/views/SettingView.kt | 21 +++--- .../kotlin/be/simplenotes}/views/UserView.kt | 27 ++++--- .../main/kotlin/be/simplenotes}/views/View.kt | 15 ++-- .../kotlin/be/simplenotes/views/ViewModule.kt | 12 +++ .../simplenotes}/views/components/Alerts.kt | 6 +- .../views/components/DeletedNoteTable.kt | 6 +- .../be/simplenotes}/views/components/Forms.kt | 6 +- .../simplenotes}/views/components/Navbar.kt | 8 +- .../views/components/NoteListHeader.kt | 4 +- .../views/components/NoteTable.kt | 6 +- .../extensions/KotlinxHtmlExtensions.kt | 6 +- .../be/simplenotes/views/utils/PrettyDate.kt | 10 +++ 37 files changed, 255 insertions(+), 227 deletions(-) delete mode 100644 simplenotes-app/src/main/kotlin/be/simplenotes/app/utils/PrettyDate.kt rename simplenotes-domain/src/test/kotlin/be/simplenotes/domain/security/{JwtPayloadExtractorTest.kt => LoggedInUserExtractorTest.kt} (91%) create mode 100644 simplenotes-views/pom.xml rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/BaseView.kt (92%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/ErrorView.kt (70%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/NoteView.kt (88%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/SettingView.kt (85%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/UserView.kt (77%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/View.kt (83%) create mode 100644 simplenotes-views/src/main/kotlin/be/simplenotes/views/ViewModule.kt rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/components/Alerts.kt (73%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/components/DeletedNoteTable.kt (88%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/components/Forms.kt (88%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/components/Navbar.kt (83%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/components/NoteListHeader.kt (88%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes}/views/components/NoteTable.kt (85%) rename {simplenotes-app/src/main/kotlin/be/simplenotes/app => simplenotes-views/src/main/kotlin/be/simplenotes/views}/extensions/KotlinxHtmlExtensions.kt (55%) create mode 100644 simplenotes-views/src/main/kotlin/be/simplenotes/views/utils/PrettyDate.kt diff --git a/pom.xml b/pom.xml index b2ec063..d4efa06 100644 --- a/pom.xml +++ b/pom.xml @@ -15,6 +15,7 @@ simplenotes-types simplenotes-config simplenotes-test-resources + simplenotes-views pom diff --git a/simplenotes-app/pom.xml b/simplenotes-app/pom.xml index 29e7a4d..903eb0e 100644 --- a/simplenotes-app/pom.xml +++ b/simplenotes-app/pom.xml @@ -65,20 +65,10 @@ compile - - org.jetbrains.kotlinx - kotlinx-html-jvm - 0.7.1 - org.jetbrains.kotlinx kotlinx-serialization-json-jvm - - org.ocpsoft.prettytime - prettytime - 4.0.5.Final - ch.qos.logback @@ -112,6 +102,12 @@ me.liuwj.ktorm ktorm-core + + be.simplenotes + simplenotes-views + 1.0-SNAPSHOT + compile + diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/SimpleNotes.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/SimpleNotes.kt index bd68ec0..836802c 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/SimpleNotes.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/SimpleNotes.kt @@ -6,6 +6,7 @@ import be.simplenotes.domain.domainModule import be.simplenotes.persistance.migrationModule import be.simplenotes.persistance.persistanceModule import be.simplenotes.search.searchModule +import be.simplenotes.views.viewModule import org.koin.core.context.startKoin import org.koin.core.context.unloadKoinModules @@ -16,10 +17,8 @@ fun main() { persistanceModule, migrationModule, configModule, - baseModule, - userModule, - noteModule, - settingsModule, + viewModule, + controllerModule, domainModule, searchModule, apiModule, diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiNoteController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiNoteController.kt index c874389..2705d8f 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiNoteController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/api/ApiNoteController.kt @@ -4,8 +4,8 @@ import be.simplenotes.app.extensions.auto import be.simplenotes.app.utils.parseSearchTerms import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNoteMetadata -import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.usecases.NoteService +import be.simplenotes.types.LoggedInUser import kotlinx.serialization.Contextual import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json @@ -20,27 +20,27 @@ import java.util.* class ApiNoteController(private val noteService: NoteService, private val json: Json) { - fun createNote(request: Request, jwtPayload: JwtPayload): Response { + fun createNote(request: Request, loggedInUser: LoggedInUser): Response { val content = noteContentLens(request) - return noteService.create(jwtPayload.userId, content).fold( + return noteService.create(loggedInUser.userId, content).fold( { Response(BAD_REQUEST) }, { uuidContentLens(UuidContent(it.uuid), Response(OK)) } ) } - fun notes(request: Request, jwtPayload: JwtPayload): Response { - val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes + fun notes(request: Request, loggedInUser: LoggedInUser): Response { + val notes = noteService.paginatedNotes(loggedInUser.userId, page = 1).notes return persistedNotesMetadataLens(notes, Response(OK)) } - fun note(request: Request, jwtPayload: JwtPayload): Response = - noteService.find(jwtPayload.userId, uuidLens(request)) + fun note(request: Request, loggedInUser: LoggedInUser): Response = + noteService.find(loggedInUser.userId, uuidLens(request)) ?.let { persistedNoteLens(it, Response(OK)) } ?: Response(NOT_FOUND) - fun update(request: Request, jwtPayload: JwtPayload): Response { + fun update(request: Request, loggedInUser: LoggedInUser): Response { val content = noteContentLens(request) - return noteService.update(jwtPayload.userId, uuidLens(request), content).fold({ + return noteService.update(loggedInUser.userId, uuidLens(request), content).fold({ Response(BAD_REQUEST) }, { if (it == null) Response(NOT_FOUND) @@ -48,10 +48,10 @@ class ApiNoteController(private val noteService: NoteService, private val json: }) } - fun search(request: Request, jwtPayload: JwtPayload): Response { + fun search(request: Request, loggedInUser: LoggedInUser): Response { val query = searchContentLens(request) val terms = parseSearchTerms(query) - val notes = noteService.search(jwtPayload.userId, terms) + val notes = noteService.search(loggedInUser.userId, terms) return persistedNotesMetadataLens(notes, Response(OK)) } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/BaseController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/BaseController.kt index 37e5134..a63cd09 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/BaseController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/BaseController.kt @@ -1,13 +1,13 @@ package be.simplenotes.app.controllers import be.simplenotes.app.extensions.html -import be.simplenotes.app.views.BaseView -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.types.LoggedInUser +import be.simplenotes.views.BaseView import org.http4k.core.Request import org.http4k.core.Response import org.http4k.core.Status.Companion.OK class BaseController(private val view: BaseView) { - fun index(@Suppress("UNUSED_PARAMETER") request: Request, jwtPayload: JwtPayload?) = - Response(OK).html(view.renderHome(jwtPayload)) + fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) = + Response(OK).html(view.renderHome(loggedInUser)) } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/NoteController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/NoteController.kt index 9c3d326..e3e25c4 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/NoteController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/NoteController.kt @@ -3,12 +3,12 @@ package be.simplenotes.app.controllers import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.redirect import be.simplenotes.app.utils.parseSearchTerms -import be.simplenotes.app.views.NoteView -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.views.NoteView import be.simplenotes.domain.usecases.NoteService import be.simplenotes.domain.usecases.markdown.InvalidMeta import be.simplenotes.domain.usecases.markdown.MissingMeta import be.simplenotes.domain.usecases.markdown.ValidationError +import be.simplenotes.types.LoggedInUser import org.http4k.core.Method import org.http4k.core.Request import org.http4k.core.Response @@ -25,18 +25,18 @@ class NoteController( private val noteService: NoteService, ) { - fun new(request: Request, jwtPayload: JwtPayload): Response { - if (request.method == Method.GET) return Response(OK).html(view.noteEditor(jwtPayload)) + fun new(request: Request, loggedInUser: LoggedInUser): Response { + if (request.method == Method.GET) return Response(OK).html(view.noteEditor(loggedInUser)) val markdownForm = request.form("markdown") ?: "" - return noteService.create(jwtPayload.userId, markdownForm).fold( + return noteService.create(loggedInUser.userId, markdownForm).fold( { val html = when (it) { - MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm) - InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm) + MissingMeta -> view.noteEditor(loggedInUser, error = "Missing note metadata", textarea = markdownForm) + InvalidMeta -> view.noteEditor(loggedInUser, error = "Invalid note metadata", textarea = markdownForm) is ValidationError -> view.noteEditor( - jwtPayload, + loggedInUser, validationErrors = it.validationErrors, textarea = markdownForm ) @@ -49,66 +49,66 @@ class NoteController( ) } - fun list(request: Request, jwtPayload: JwtPayload): Response { + fun list(request: Request, loggedInUser: LoggedInUser): Response { val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1 val tag = request.query("tag") - val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag) - val deletedCount = noteService.countDeleted(jwtPayload.userId) - return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages, deletedCount, tag = tag)) + val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag) + val deletedCount = noteService.countDeleted(loggedInUser.userId) + return Response(OK).html(view.notes(loggedInUser, notes, currentPage, pages, deletedCount, tag = tag)) } - fun search(request: Request, jwtPayload: JwtPayload): Response { + fun search(request: Request, loggedInUser: LoggedInUser): Response { val query = request.form("search") ?: "" val terms = parseSearchTerms(query) - val notes = noteService.search(jwtPayload.userId, terms) - val deletedCount = noteService.countDeleted(jwtPayload.userId) - return Response(OK).html(view.search(jwtPayload, notes, query, deletedCount)) + val notes = noteService.search(loggedInUser.userId, terms) + val deletedCount = noteService.countDeleted(loggedInUser.userId) + return Response(OK).html(view.search(loggedInUser, notes, query, deletedCount)) } - fun note(request: Request, jwtPayload: JwtPayload): Response { + fun note(request: Request, loggedInUser: LoggedInUser): Response { val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND) if (request.method == Method.POST) { if (request.form("delete") != null) { - return if (noteService.trash(jwtPayload.userId, noteUuid)) + return if (noteService.trash(loggedInUser.userId, noteUuid)) Response.redirect("/notes") // TODO: flash cookie to show success ? else Response(NOT_FOUND) // TODO: show an error } if (request.form("public") != null) { - if (!noteService.makePublic(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND) + if (!noteService.makePublic(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND) } else if (request.form("private") != null) { - if (!noteService.makePrivate(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND) + if (!noteService.makePrivate(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND) } } - val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND) - return Response(OK).html(view.renderedNote(jwtPayload, note, shared = false)) + val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND) + return Response(OK).html(view.renderedNote(loggedInUser, note, shared = false)) } - fun public(request: Request, jwtPayload: JwtPayload?): Response { + fun public(request: Request, loggedInUser: LoggedInUser?): Response { val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND) val note = noteService.findPublic(noteUuid) ?: return Response(NOT_FOUND) - return Response(OK).html(view.renderedNote(jwtPayload, note, shared = true)) + return Response(OK).html(view.renderedNote(loggedInUser, note, shared = true)) } - fun edit(request: Request, jwtPayload: JwtPayload): Response { + fun edit(request: Request, loggedInUser: LoggedInUser): Response { val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND) - val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND) + val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND) if (request.method == Method.GET) { - return Response(OK).html(view.noteEditor(jwtPayload, textarea = note.markdown)) + return Response(OK).html(view.noteEditor(loggedInUser, textarea = note.markdown)) } val markdownForm = request.form("markdown") ?: "" - return noteService.update(jwtPayload.userId, note.uuid, markdownForm).fold( + return noteService.update(loggedInUser.userId, note.uuid, markdownForm).fold( { val html = when (it) { - MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm) - InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm) + MissingMeta -> view.noteEditor(loggedInUser, error = "Missing note metadata", textarea = markdownForm) + InvalidMeta -> view.noteEditor(loggedInUser, error = "Invalid note metadata", textarea = markdownForm) is ValidationError -> view.noteEditor( - jwtPayload, + loggedInUser, validationErrors = it.validationErrors, textarea = markdownForm ) @@ -121,21 +121,21 @@ class NoteController( ) } - fun trash(request: Request, jwtPayload: JwtPayload): Response { + fun trash(request: Request, loggedInUser: LoggedInUser): Response { val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1 val tag = request.query("tag") - val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag, deleted = true) - return Response(OK).html(view.trash(jwtPayload, notes, currentPage, pages)) + val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag, deleted = true) + return Response(OK).html(view.trash(loggedInUser, notes, currentPage, pages)) } - fun deleted(request: Request, jwtPayload: JwtPayload): Response { + fun deleted(request: Request, loggedInUser: LoggedInUser): Response { val uuid = request.uuidPath() ?: return Response(NOT_FOUND) return if (request.form("delete") != null) - if (noteService.delete(jwtPayload.userId, uuid)) + if (noteService.delete(loggedInUser.userId, uuid)) Response.redirect("/notes/trash") else Response(NOT_FOUND) - else if (noteService.restore(jwtPayload.userId, uuid)) + else if (noteService.restore(loggedInUser.userId, uuid)) Response.redirect("/notes/$uuid") else Response(NOT_FOUND) diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/SettingsController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/SettingsController.kt index 51d5d6a..9e16ee3 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/SettingsController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/SettingsController.kt @@ -2,11 +2,11 @@ package be.simplenotes.app.controllers import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.redirect -import be.simplenotes.app.views.SettingView -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.views.SettingView import be.simplenotes.domain.usecases.UserService import be.simplenotes.domain.usecases.users.delete.DeleteError import be.simplenotes.domain.usecases.users.delete.DeleteForm +import be.simplenotes.types.LoggedInUser import org.http4k.core.* import org.http4k.core.body.form import org.http4k.core.cookie.invalidateCookie @@ -15,11 +15,11 @@ class SettingsController( private val userService: UserService, private val settingView: SettingView, ) { - fun settings(request: Request, jwtPayload: JwtPayload): Response { + fun settings(request: Request, loggedInUser: LoggedInUser): Response { if (request.method == Method.GET) - return Response(Status.OK).html(settingView.settings(jwtPayload)) + return Response(Status.OK).html(settingView.settings(loggedInUser)) - val deleteForm = request.deleteForm(jwtPayload) + val deleteForm = request.deleteForm(loggedInUser) val result = userService.delete(deleteForm) return result.fold( @@ -28,13 +28,13 @@ class SettingsController( DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer") DeleteError.WrongPassword -> Response(Status.OK).html( settingView.settings( - jwtPayload, + loggedInUser, error = "Wrong password" ) ) is DeleteError.InvalidForm -> Response(Status.OK).html( settingView.settings( - jwtPayload, + loggedInUser, validationErrors = it.validationErrors ) ) @@ -53,23 +53,23 @@ class SettingsController( .header("Content-Type", contentType) } - fun export(request: Request, jwtPayload: JwtPayload): Response { + fun export(request: Request, loggedInUser: LoggedInUser): Response { val isDownload = request.form("download") != null return if (isDownload) { - val filename = "simplenotes-export-${jwtPayload.username}" + val filename = "simplenotes-export-${loggedInUser.username}" if (request.form("format") == "zip") { - val zip = userService.exportAsZip(jwtPayload.userId) + val zip = userService.exportAsZip(loggedInUser.userId) Response(Status.OK) .with(attachment("$filename.zip", "application/zip")) .body(zip) } else Response(Status.OK) .with(attachment("$filename.json", "application/json")) - .body(userService.exportAsJson(jwtPayload.userId)) - } else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json") + .body(userService.exportAsJson(loggedInUser.userId)) + } else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header("Content-Type", "application/json") } - private fun Request.deleteForm(jwtPayload: JwtPayload) = - DeleteForm(jwtPayload.username, form("password"), form("checked") != null) + private fun Request.deleteForm(loggedInUser: LoggedInUser) = + DeleteForm(loggedInUser.username, form("password"), form("checked") != null) } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/UserController.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/UserController.kt index d223955..bb98ed9 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/UserController.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/controllers/UserController.kt @@ -3,14 +3,14 @@ package be.simplenotes.app.controllers import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.isSecure import be.simplenotes.app.extensions.redirect -import be.simplenotes.app.views.UserView -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.views.UserView import be.simplenotes.domain.usecases.UserService import be.simplenotes.domain.usecases.users.login.* import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm import be.simplenotes.domain.usecases.users.register.RegisterForm import be.simplenotes.domain.usecases.users.register.UserExists import be.simplenotes.config.JwtConfig +import be.simplenotes.types.LoggedInUser import org.http4k.core.Method.GET import org.http4k.core.Request import org.http4k.core.Response @@ -27,9 +27,9 @@ class UserController( private val userView: UserView, private val jwtConfig: JwtConfig, ) { - fun register(request: Request, jwtPayload: JwtPayload?): Response { + fun register(request: Request, loggedInUser: LoggedInUser?): Response { if (request.method == GET) return Response(OK).html( - userView.register(jwtPayload) + userView.register(loggedInUser) ) val result = userService.register(request.registerForm()) @@ -38,12 +38,12 @@ class UserController( { val html = when (it) { UserExists -> userView.register( - jwtPayload, + loggedInUser, error = "User already exists" ) is InvalidRegisterForm -> userView.register( - jwtPayload, + loggedInUser, validationErrors = it.validationErrors ) } @@ -58,9 +58,9 @@ class UserController( private fun Request.registerForm() = RegisterForm(form("username"), form("password")) private fun Request.loginForm(): LoginForm = registerForm() - fun login(request: Request, jwtPayload: JwtPayload?): Response { + fun login(request: Request, loggedInUser: LoggedInUser?): Response { if (request.method == GET) return Response(OK).html( - userView.login(jwtPayload) + userView.login(loggedInUser) ) val result = userService.login(request.loginForm()) @@ -70,17 +70,17 @@ class UserController( val html = when (it) { Unregistered -> userView.login( - jwtPayload, + loggedInUser, error = "User does not exist" ) WrongPassword -> userView.login( - jwtPayload, + loggedInUser, error = "Wrong password" ) is InvalidLoginForm -> userView.login( - jwtPayload, + loggedInUser, validationErrors = it.validationErrors ) } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/AuthFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/AuthFilter.kt index 98f64f0..b080de4 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/AuthFilter.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/AuthFilter.kt @@ -1,8 +1,8 @@ package be.simplenotes.app.filters import be.simplenotes.app.extensions.redirect -import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayloadExtractor +import be.simplenotes.types.LoggedInUser import org.http4k.core.* import org.http4k.core.Status.Companion.UNAUTHORIZED import org.http4k.core.cookie.cookie @@ -40,7 +40,7 @@ class AuthFilter( } } -fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey] +fun Request.jwtPayload(ctx: RequestContexts): LoggedInUser? = ctx[this][authKey] enum class JwtSource { Header, Cookie diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/ErrorFilter.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/ErrorFilter.kt index 21f8ece..a80eb1d 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/ErrorFilter.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/filters/ErrorFilter.kt @@ -1,8 +1,8 @@ package be.simplenotes.app.filters import be.simplenotes.app.extensions.html -import be.simplenotes.app.views.ErrorView -import be.simplenotes.app.views.ErrorView.Type.* +import be.simplenotes.views.ErrorView +import be.simplenotes.views.ErrorView.Type.* import org.http4k.core.* import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR import org.http4k.core.Status.Companion.NOT_FOUND diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ConfigModule.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ConfigModule.kt index b860224..4699451 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ConfigModule.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ConfigModule.kt @@ -1,6 +1,8 @@ package be.simplenotes.app.modules import be.simplenotes.app.Config +import be.simplenotes.app.utils.StaticFileResolver +import org.koin.core.qualifier.named import org.koin.dsl.module val configModule = module { diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/CoreModules.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/CoreModules.kt index 8961883..82f2f6c 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/CoreModules.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/CoreModules.kt @@ -1,29 +1,12 @@ package be.simplenotes.app.modules import be.simplenotes.app.controllers.* -import be.simplenotes.app.views.BaseView -import be.simplenotes.app.views.NoteView -import be.simplenotes.app.views.SettingView -import be.simplenotes.app.views.UserView import org.koin.dsl.module -val userModule = module { +val controllerModule = module { single { UserController(get(), get(), get()) } - single { UserView(get()) } -} - -val baseModule = module { single { HealthCheckController(get()) } single { BaseController(get()) } - single { BaseView(get()) } -} - -val noteModule = module { single { NoteController(get(), get()) } - single { NoteView(get()) } -} - -val settingsModule = module { single { SettingsController(get(), get()) } - single { SettingView(get()) } } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ServerModule.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ServerModule.kt index 421fa9f..0f5707f 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ServerModule.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/modules/ServerModule.kt @@ -10,7 +10,7 @@ import be.simplenotes.app.jetty.Jetty import be.simplenotes.app.routes.Router import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolverImpl -import be.simplenotes.app.views.ErrorView +import be.simplenotes.views.ErrorView import be.simplenotes.config.ServerConfig import org.eclipse.jetty.server.ServerConnector import org.http4k.core.Filter @@ -59,5 +59,5 @@ val serverModule = module { single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) } single { ErrorFilter(get()) } single { TransactionFilter(get()) } - single { ErrorView(get()) } + single(named("styles")) { get().resolve("styles.css") } } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/Router.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/Router.kt index 4caf064..d0862f4 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/Router.kt +++ b/simplenotes-app/src/main/kotlin/be/simplenotes/app/routes/Router.kt @@ -4,7 +4,7 @@ import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.api.ApiUserController import be.simplenotes.app.controllers.* import be.simplenotes.app.filters.* -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.types.LoggedInUser import org.http4k.core.* import org.http4k.core.Method.* import org.http4k.filter.ResponseFilters.GZip @@ -102,5 +102,5 @@ class Router( this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) } } -private typealias PublicHandler = (Request, JwtPayload?) -> Response -private typealias ProtectedHandler = (Request, JwtPayload) -> Response +private typealias PublicHandler = (Request, LoggedInUser?) -> Response +private typealias ProtectedHandler = (Request, LoggedInUser) -> Response diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/utils/PrettyDate.kt b/simplenotes-app/src/main/kotlin/be/simplenotes/app/utils/PrettyDate.kt deleted file mode 100644 index 9294e9a..0000000 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/utils/PrettyDate.kt +++ /dev/null @@ -1,10 +0,0 @@ -package be.simplenotes.app.utils - -import org.ocpsoft.prettytime.PrettyTime -import java.time.LocalDateTime -import java.time.ZoneId -import java.util.* - -private val prettyTime = PrettyTime() - -fun LocalDateTime.toTimeAgo(): String = prettyTime.format(Date.from(atZone(ZoneId.systemDefault()).toInstant())) diff --git a/simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/AuthFilterTest.kt b/simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/AuthFilterTest.kt index 929542d..30ecc74 100644 --- a/simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/AuthFilterTest.kt +++ b/simplenotes-app/src/test/kotlin/be/simplenotes/app/filters/AuthFilterTest.kt @@ -1,9 +1,9 @@ package be.simplenotes.app.filters -import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayloadExtractor import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.config.JwtConfig +import be.simplenotes.types.LoggedInUser import com.natpryce.hamkrest.assertion.assertThat import org.http4k.core.* import org.http4k.core.Method.GET @@ -58,7 +58,7 @@ internal class AuthFilterTest { @Test fun `it should allow a valid token`() { - val jwtPayload = JwtPayload(1, "user") + val jwtPayload = LoggedInUser(1, "user") val token = simpleJwt.sign(jwtPayload) val response = app(Request(GET, "/optional").cookie("Bearer", token)) assertThat(response, hasStatus(OK)) @@ -84,7 +84,7 @@ internal class AuthFilterTest { @Test fun `it should allow a valid token"`() { - val jwtPayload = JwtPayload(1, "user") + val jwtPayload = LoggedInUser(1, "user") val token = simpleJwt.sign(jwtPayload) val response = app(Request(GET, "/protected").cookie("Bearer", token)) assertThat(response, hasStatus(OK)) diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/JwtPayload.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/JwtPayload.kt index b90f2ff..e316bdc 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/JwtPayload.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/JwtPayload.kt @@ -1,18 +1,14 @@ package be.simplenotes.domain.security -import be.simplenotes.types.PersistedUser +import be.simplenotes.types.LoggedInUser import com.auth0.jwt.exceptions.JWTVerificationException -data class JwtPayload(val userId: Int, val username: String) { - constructor(user: PersistedUser) : this(user.id, user.username) -} - class JwtPayloadExtractor(private val jwt: SimpleJwt) { - operator fun invoke(token: String): JwtPayload? = try { + operator fun invoke(token: String): LoggedInUser? = try { val decodedJWT = jwt.verifier.verify(token) val id = decodedJWT.getClaim(userIdField).asInt() ?: null val username = decodedJWT.getClaim(usernameField).asString() ?: null - id?.let { username?.let { JwtPayload(id, username) } } + id?.let { username?.let { LoggedInUser(id, username) } } } catch (e: JWTVerificationException) { null } catch (e: IllegalArgumentException) { diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/SimpleJwt.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/SimpleJwt.kt index 0fde615..e8ced0f 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/SimpleJwt.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/security/SimpleJwt.kt @@ -1,6 +1,7 @@ package be.simplenotes.domain.security import be.simplenotes.config.JwtConfig +import be.simplenotes.types.LoggedInUser import com.auth0.jwt.JWT import com.auth0.jwt.JWTVerifier import com.auth0.jwt.algorithms.Algorithm @@ -15,9 +16,9 @@ class SimpleJwt(jwtConfig: JwtConfig) { private val algorithm = Algorithm.HMAC256(jwtConfig.secret) val verifier: JWTVerifier = JWT.require(algorithm).build() - fun sign(jwtPayload: JwtPayload): String = JWT.create() - .withClaim(userIdField, jwtPayload.userId) - .withClaim(usernameField, jwtPayload.username) + fun sign(loggedInUser: LoggedInUser): String = JWT.create() + .withClaim(userIdField, loggedInUser.userId) + .withClaim(usernameField, loggedInUser.username) .withExpiresAt(getExpiration()) .sign(algorithm) diff --git a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/login/LoginUseCaseImpl.kt b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/login/LoginUseCaseImpl.kt index cfef653..2190dac 100644 --- a/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/login/LoginUseCaseImpl.kt +++ b/simplenotes-domain/src/main/kotlin/be/simplenotes/domain/usecases/users/login/LoginUseCaseImpl.kt @@ -4,11 +4,11 @@ import arrow.core.Either import arrow.core.extensions.fx import arrow.core.filterOrElse import arrow.core.rightIfNotNull -import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.validation.UserValidations import be.simplenotes.persistance.repositories.UserRepository +import be.simplenotes.types.LoggedInUser internal class LoginUseCaseImpl( private val userRepository: UserRepository, @@ -20,6 +20,6 @@ internal class LoginUseCaseImpl( !userRepository.find(user.username) .rightIfNotNull { Unregistered } .filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword }) - .map { jwt.sign(JwtPayload(it)) } + .map { jwt.sign(LoggedInUser(it)) } } } diff --git a/simplenotes-domain/src/test/kotlin/be/simplenotes/domain/security/JwtPayloadExtractorTest.kt b/simplenotes-domain/src/test/kotlin/be/simplenotes/domain/security/LoggedInUserExtractorTest.kt similarity index 91% rename from simplenotes-domain/src/test/kotlin/be/simplenotes/domain/security/JwtPayloadExtractorTest.kt rename to simplenotes-domain/src/test/kotlin/be/simplenotes/domain/security/LoggedInUserExtractorTest.kt index 0f09b32..94b6116 100644 --- a/simplenotes-domain/src/test/kotlin/be/simplenotes/domain/security/JwtPayloadExtractorTest.kt +++ b/simplenotes-domain/src/test/kotlin/be/simplenotes/domain/security/LoggedInUserExtractorTest.kt @@ -2,6 +2,7 @@ package be.simplenotes.domain.security import be.simplenotes.domain.usecases.users.login.Token import be.simplenotes.config.JwtConfig +import be.simplenotes.types.LoggedInUser import com.auth0.jwt.JWT import com.auth0.jwt.algorithms.Algorithm import com.natpryce.hamkrest.absent @@ -13,7 +14,7 @@ import org.junit.jupiter.params.provider.MethodSource import java.util.concurrent.TimeUnit import java.util.stream.Stream -internal class JwtPayloadExtractorTest { +internal class LoggedInUserExtractorTest { private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS) private val simpleJwt = SimpleJwt(jwtConfig) private val jwtPayloadExtractor = JwtPayloadExtractor(simpleJwt) @@ -45,6 +46,6 @@ internal class JwtPayloadExtractorTest { @Test fun `parse valid token`() { val token = createToken(username = "someone", id = 1) - assertThat(jwtPayloadExtractor(token), equalTo(JwtPayload(1, "someone"))) + assertThat(jwtPayloadExtractor(token), equalTo(LoggedInUser(1, "someone"))) } } diff --git a/simplenotes-types/src/main/kotlin/be/simplenotes/types/User.kt b/simplenotes-types/src/main/kotlin/be/simplenotes/types/User.kt index bd93f58..14356cd 100644 --- a/simplenotes-types/src/main/kotlin/be/simplenotes/types/User.kt +++ b/simplenotes-types/src/main/kotlin/be/simplenotes/types/User.kt @@ -2,3 +2,6 @@ package be.simplenotes.types data class User(val username: String, val password: String) data class PersistedUser(val username: String, val password: String, val id: Int) +data class LoggedInUser(val userId: Int, val username: String) { + constructor(user: PersistedUser) : this(user.id, user.username) +} diff --git a/simplenotes-views/pom.xml b/simplenotes-views/pom.xml new file mode 100644 index 0000000..401cd17 --- /dev/null +++ b/simplenotes-views/pom.xml @@ -0,0 +1,41 @@ + + + + simplenotes-parent + be.simplenotes + 1.0-SNAPSHOT + + 4.0.0 + + simplenotes-views + + + + org.koin + koin-core + + + io.konform + konform-jvm + 0.2.0 + + + org.jetbrains.kotlinx + kotlinx-html-jvm + 0.7.1 + + + org.ocpsoft.prettytime + prettytime + 4.0.5.Final + + + be.simplenotes + simplenotes-types + 1.0-SNAPSHOT + + + + diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/BaseView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/BaseView.kt similarity index 92% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/BaseView.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/BaseView.kt index f911cf3..64f95ea 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/BaseView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/BaseView.kt @@ -1,15 +1,14 @@ -package be.simplenotes.app.views +package be.simplenotes.views -import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.types.LoggedInUser import kotlinx.html.* import kotlinx.html.ThScope.col -class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { - fun renderHome(jwtPayload: JwtPayload?) = renderPage( +class BaseView(styles: String) : View(styles) { + fun renderHome(loggedInUser: LoggedInUser?) = renderPage( title = "Home", description = "A fast and simple note taking website", - jwtPayload = jwtPayload + loggedInUser = loggedInUser ) { section("text-center my-2 p-2") { h1("text-5xl casual") { diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/ErrorView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/ErrorView.kt similarity index 70% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/ErrorView.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/ErrorView.kt index 4f6c830..58e1f50 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/ErrorView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/ErrorView.kt @@ -1,12 +1,11 @@ -package be.simplenotes.app.views +package be.simplenotes.views -import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.app.views.components.Alert -import be.simplenotes.app.views.components.alert +import be.simplenotes.views.components.Alert +import be.simplenotes.views.components.alert import kotlinx.html.a import kotlinx.html.div -class ErrorView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { +class ErrorView(styles: String) : View(styles) { enum class Type(val title: String) { SqlTransientError("Database unavailable"), @@ -14,7 +13,7 @@ class ErrorView(staticFileResolver: StaticFileResolver) : View(staticFileResolve Other("Error"), } - fun error(errorType: Type) = renderPage(errorType.title, jwtPayload = null) { + fun error(errorType: Type) = renderPage(errorType.title, loggedInUser = null) { div("container mx-auto p-4") { when (errorType) { Type.SqlTransientError -> alert( diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/NoteView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/NoteView.kt similarity index 88% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/NoteView.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/NoteView.kt index ccfadb0..8c7fc30 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/NoteView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/NoteView.kt @@ -1,21 +1,21 @@ -package be.simplenotes.app.views +package be.simplenotes.views -import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.app.views.components.* +import be.simplenotes.types.LoggedInUser +import be.simplenotes.views.components.noteListHeader import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNoteMetadata -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.views.components.* import io.konform.validation.ValidationError import kotlinx.html.* -class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { +class NoteView(styles: String) : View(styles) { fun noteEditor( - jwtPayload: JwtPayload, + loggedInUser: LoggedInUser, error: String? = null, textarea: String? = null, validationErrors: List = emptyList(), - ) = renderPage(title = "New note", jwtPayload = jwtPayload) { + ) = renderPage(title = "New note", loggedInUser = loggedInUser) { div("container mx-auto p-4") { error?.let { alert(Alert.Warning, error) } validationErrors.forEach { @@ -46,13 +46,13 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } fun notes( - jwtPayload: JwtPayload, + loggedInUser: LoggedInUser, notes: List, currentPage: Int, numberOfPages: Int, numberOfDeletedNotes: Int, tag: String?, - ) = renderPage(title = "Notes", jwtPayload = jwtPayload) { + ) = renderPage(title = "Notes", loggedInUser = loggedInUser) { div("container mx-auto p-4") { noteListHeader(numberOfDeletedNotes) if (notes.isNotEmpty()) @@ -68,11 +68,11 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } fun search( - jwtPayload: JwtPayload, + loggedInUser: LoggedInUser, notes: List, query: String, numberOfDeletedNotes: Int, - ) = renderPage("Notes", jwtPayload = jwtPayload) { + ) = renderPage("Notes", loggedInUser = loggedInUser) { div("container mx-auto p-4") { noteListHeader(numberOfDeletedNotes, query) noteTable(notes) @@ -80,11 +80,11 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } fun trash( - jwtPayload: JwtPayload, + loggedInUser: LoggedInUser, notes: List, currentPage: Int, numberOfPages: Int, - ) = renderPage(title = "Notes", jwtPayload = jwtPayload) { + ) = renderPage(title = "Notes", loggedInUser = loggedInUser) { div("container mx-auto p-4") { div("flex justify-between mb-4") { h1("text-2xl underline") { +"Deleted notes" } @@ -116,9 +116,9 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } } - fun renderedNote(jwtPayload: JwtPayload?, note: PersistedNote, shared: Boolean) = renderPage( + fun renderedNote(loggedInUser: LoggedInUser?, note: PersistedNote, shared: Boolean) = renderPage( note.meta.title, - jwtPayload = jwtPayload, + loggedInUser = loggedInUser, scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js") ) { div("container mx-auto p-4") { diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/SettingView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/SettingView.kt similarity index 85% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/SettingView.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/SettingView.kt index 05a313e..8c18203 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/SettingView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/SettingView.kt @@ -1,28 +1,27 @@ -package be.simplenotes.app.views +package be.simplenotes.views -import be.simplenotes.app.extensions.summary -import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.app.views.components.Alert -import be.simplenotes.app.views.components.alert -import be.simplenotes.app.views.components.input -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.types.LoggedInUser +import be.simplenotes.views.components.Alert +import be.simplenotes.views.components.alert +import be.simplenotes.views.components.input +import be.simplenotes.views.extensions.summary import io.konform.validation.ValidationError import kotlinx.html.* import kotlinx.html.ButtonType.submit -class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { +class SettingView(styles: String) : View(styles) { fun settings( - jwtPayload: JwtPayload, + loggedInUser: LoggedInUser, error: String? = null, validationErrors: List = emptyList(), - ) = renderPage("Settings", jwtPayload = jwtPayload) { + ) = renderPage("Settings", loggedInUser = loggedInUser) { div("container mx-auto") { section("m-4 p-4 bg-gray-800 rounded") { h1("text-xl") { +"Welcome " - span("text-teal-200 font-semibold") { +jwtPayload.username } + span("text-teal-200 font-semibold") { +loggedInUser.username } } } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/UserView.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/UserView.kt similarity index 77% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/UserView.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/UserView.kt index 9574227..b8bbdf6 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/UserView.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/UserView.kt @@ -1,23 +1,22 @@ -package be.simplenotes.app.views +package be.simplenotes.views -import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.app.views.components.Alert -import be.simplenotes.app.views.components.alert -import be.simplenotes.app.views.components.input -import be.simplenotes.app.views.components.submitButton -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.types.LoggedInUser +import be.simplenotes.views.components.Alert +import be.simplenotes.views.components.alert +import be.simplenotes.views.components.input +import be.simplenotes.views.components.submitButton import io.konform.validation.ValidationError import kotlinx.html.* -class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { +class UserView(styles: String) : View(styles) { fun register( - jwtPayload: JwtPayload?, + loggedInUser: LoggedInUser?, error: String? = null, validationErrors: List = emptyList(), ) = accountForm( "Register", "Registration page", - jwtPayload, + loggedInUser, error, validationErrors, "Create an account", @@ -28,11 +27,11 @@ class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } fun login( - jwtPayload: JwtPayload?, + loggedInUser: LoggedInUser?, error: String? = null, validationErrors: List = emptyList(), new: Boolean = false, - ) = accountForm("Login", "Login page", jwtPayload, error, validationErrors, "Sign In", "Sign In", new) { + ) = accountForm("Login", "Login page", loggedInUser, error, validationErrors, "Sign In", "Sign In", new) { +"Don't have an account yet? " a(href = "/register", classes = "no-underline text-blue-500 hover:text-blue-400 font-bold") { +"Create an account" @@ -42,14 +41,14 @@ class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver private fun accountForm( title: String, description: String, - jwtPayload: JwtPayload?, + loggedInUser: LoggedInUser?, error: String? = null, validationErrors: List = emptyList(), h1: String, submit: String, new: Boolean = false, footer: FlowContent.() -> Unit, - ) = renderPage(title = title, description, jwtPayload = jwtPayload) { + ) = renderPage(title = title, description, loggedInUser = loggedInUser) { div("centered container mx-auto flex justify-center items-center") { div("w-full md:w-1/2 lg:w-1/3 m-4") { div("p-8 mb-6") { diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/View.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/View.kt similarity index 83% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/View.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/View.kt index 998e19f..959cd42 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/View.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/View.kt @@ -1,19 +1,16 @@ -package be.simplenotes.app.views +package be.simplenotes.views -import be.simplenotes.app.utils.StaticFileResolver -import be.simplenotes.app.views.components.navbar -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.types.LoggedInUser +import be.simplenotes.views.components.navbar import kotlinx.html.* import kotlinx.html.stream.appendHTML -abstract class View(staticFileResolver: StaticFileResolver) { - - private val styles = staticFileResolver.resolve("styles.css")!! +abstract class View(private val styles: String) { fun renderPage( title: String, description: String? = null, - jwtPayload: JwtPayload?, + loggedInUser: LoggedInUser?, scripts: List = emptyList(), body: MAIN.() -> Unit = {}, ) = buildString { @@ -37,7 +34,7 @@ abstract class View(staticFileResolver: StaticFileResolver) { } } body("bg-gray-900 text-white") { - navbar(jwtPayload) + navbar(loggedInUser) main { body() } } } diff --git a/simplenotes-views/src/main/kotlin/be/simplenotes/views/ViewModule.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/ViewModule.kt new file mode 100644 index 0000000..27699a4 --- /dev/null +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/ViewModule.kt @@ -0,0 +1,12 @@ +package be.simplenotes.views + +import org.koin.core.qualifier.named +import org.koin.dsl.module + +val viewModule = module { + single { ErrorView(get(named("styles"))) } + single { UserView(get(named("styles"))) } + single { BaseView(get(named("styles"))) } + single { SettingView(get(named("styles"))) } + single { NoteView(get(named("styles"))) } +} diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Alerts.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Alerts.kt similarity index 73% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Alerts.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Alerts.kt index 7fcb929..cac8691 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Alerts.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Alerts.kt @@ -1,8 +1,8 @@ -package be.simplenotes.app.views.components +package be.simplenotes.views.components import kotlinx.html.* -fun FlowContent.alert(type: Alert, title: String, details: String? = null, multiline: Boolean = false) { +internal fun FlowContent.alert(type: Alert, title: String, details: String? = null, multiline: Boolean = false) { val colors = when (type) { Alert.Success -> "bg-green-500 border border-green-400 text-gray-800" Alert.Warning -> "bg-red-500 border border-red-400 text-red-200" @@ -17,6 +17,6 @@ fun FlowContent.alert(type: Alert, title: String, details: String? = null, multi } } -enum class Alert { +internal enum class Alert { Success, Warning } diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/DeletedNoteTable.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/DeletedNoteTable.kt similarity index 88% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/DeletedNoteTable.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/components/DeletedNoteTable.kt index ee00851..d8ae6b2 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/DeletedNoteTable.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/DeletedNoteTable.kt @@ -1,13 +1,13 @@ -package be.simplenotes.app.views.components +package be.simplenotes.views.components -import be.simplenotes.app.utils.toTimeAgo import be.simplenotes.types.PersistedNoteMetadata +import be.simplenotes.views.utils.toTimeAgo import kotlinx.html.* import kotlinx.html.ButtonType.submit import kotlinx.html.FormMethod.post import kotlinx.html.ThScope.col -fun FlowContent.deletedNoteTable(notes: List) = div("overflow-x-auto") { +internal fun FlowContent.deletedNoteTable(notes: List) = div("overflow-x-auto") { table { id = "notes" thead { diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Forms.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Forms.kt similarity index 88% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Forms.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Forms.kt index 38faeec..92d6df5 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Forms.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Forms.kt @@ -1,9 +1,9 @@ -package be.simplenotes.app.views.components +package be.simplenotes.views.components import kotlinx.html.* import kotlinx.html.ButtonType.submit -fun FlowContent.input( +internal fun FlowContent.input( type: InputType = InputType.text, placeholder: String, id: String, @@ -26,7 +26,7 @@ fun FlowContent.input( } } -fun FlowContent.submitButton(text: String) { +internal fun FlowContent.submitButton(text: String) { div("flex items-center mt-6") { button( type = submit, diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Navbar.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Navbar.kt similarity index 83% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Navbar.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Navbar.kt index 864cf92..585a839 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/Navbar.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/Navbar.kt @@ -1,9 +1,9 @@ -package be.simplenotes.app.views.components +package be.simplenotes.views.components -import be.simplenotes.domain.security.JwtPayload +import be.simplenotes.types.LoggedInUser import kotlinx.html.* -fun BODY.navbar(jwtPayload: JwtPayload?) { +internal fun BODY.navbar(loggedInUser: LoggedInUser?) { nav { id = "navbar" a("/") { @@ -12,7 +12,7 @@ fun BODY.navbar(jwtPayload: JwtPayload?) { } ul("space-x-2") { id = "navigation" - if (jwtPayload != null) { + if (loggedInUser != null) { val links = listOf( "/notes" to "Notes", "/settings" to "Settings", diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/NoteListHeader.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/NoteListHeader.kt similarity index 88% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/NoteListHeader.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/components/NoteListHeader.kt index 25b9a41..682f054 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/NoteListHeader.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/NoteListHeader.kt @@ -1,10 +1,10 @@ -package be.simplenotes.app.views.components +package be.simplenotes.views.components import kotlinx.html.* import kotlinx.html.ButtonType.submit import kotlinx.html.FormMethod.post -fun DIV.noteListHeader(numberOfDeletedNotes: Int, query: String = "") { +internal fun DIV.noteListHeader(numberOfDeletedNotes: Int, query: String = "") { div("flex justify-between mb-4") { h1("text-2xl underline") { +"Notes" } span { diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/NoteTable.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/NoteTable.kt similarity index 85% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/NoteTable.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/components/NoteTable.kt index a44bb62..5915287 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/views/components/NoteTable.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/components/NoteTable.kt @@ -1,11 +1,11 @@ -package be.simplenotes.app.views.components +package be.simplenotes.views.components -import be.simplenotes.app.utils.toTimeAgo import be.simplenotes.types.PersistedNoteMetadata +import be.simplenotes.views.utils.toTimeAgo import kotlinx.html.* import kotlinx.html.ThScope.col -fun FlowContent.noteTable(notes: List) = div("overflow-x-auto") { +internal fun FlowContent.noteTable(notes: List) = div("overflow-x-auto") { table { id = "notes" thead { diff --git a/simplenotes-app/src/main/kotlin/be/simplenotes/app/extensions/KotlinxHtmlExtensions.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/extensions/KotlinxHtmlExtensions.kt similarity index 55% rename from simplenotes-app/src/main/kotlin/be/simplenotes/app/extensions/KotlinxHtmlExtensions.kt rename to simplenotes-views/src/main/kotlin/be/simplenotes/views/extensions/KotlinxHtmlExtensions.kt index ffc5678..0212341 100644 --- a/simplenotes-app/src/main/kotlin/be/simplenotes/app/extensions/KotlinxHtmlExtensions.kt +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/extensions/KotlinxHtmlExtensions.kt @@ -1,8 +1,8 @@ -package be.simplenotes.app.extensions +package be.simplenotes.views.extensions import kotlinx.html.* -class SUMMARY(consumer: TagConsumer<*>) : +internal class SUMMARY(consumer: TagConsumer<*>) : HTMLTag( "summary", consumer, emptyMap(), inlineTag = true, @@ -10,6 +10,6 @@ class SUMMARY(consumer: TagConsumer<*>) : ), HtmlInlineTag -fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) { +internal fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) { SUMMARY(consumer).visit(block) } diff --git a/simplenotes-views/src/main/kotlin/be/simplenotes/views/utils/PrettyDate.kt b/simplenotes-views/src/main/kotlin/be/simplenotes/views/utils/PrettyDate.kt new file mode 100644 index 0000000..40fdbf3 --- /dev/null +++ b/simplenotes-views/src/main/kotlin/be/simplenotes/views/utils/PrettyDate.kt @@ -0,0 +1,10 @@ +package be.simplenotes.views.utils + +import org.ocpsoft.prettytime.PrettyTime +import java.time.LocalDateTime +import java.time.ZoneId +import java.util.* + +private val prettyTime = PrettyTime() + +internal fun LocalDateTime.toTimeAgo(): String = prettyTime.format(Date.from(atZone(ZoneId.systemDefault()).toInstant()))