Separate views into a maven module

This commit is contained in:
Hubert Van De Walle 2020-10-24 01:25:25 +02:00
parent 536c6e7b79
commit 8b8dbd6fe5
37 changed files with 255 additions and 227 deletions

View File

@ -15,6 +15,7 @@
<module>simplenotes-types</module> <module>simplenotes-types</module>
<module>simplenotes-config</module> <module>simplenotes-config</module>
<module>simplenotes-test-resources</module> <module>simplenotes-test-resources</module>
<module>simplenotes-views</module>
</modules> </modules>
<packaging>pom</packaging> <packaging>pom</packaging>

View File

@ -65,20 +65,10 @@
<scope>compile</scope> <scope>compile</scope>
</dependency> </dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-html-jvm</artifactId>
<version>0.7.1</version>
</dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json-jvm</artifactId> <artifactId>kotlinx-serialization-json-jvm</artifactId>
</dependency> </dependency>
<dependency>
<groupId>org.ocpsoft.prettytime</groupId>
<artifactId>prettytime</artifactId>
<version>4.0.5.Final</version>
</dependency>
<dependency> <dependency>
<groupId>ch.qos.logback</groupId> <groupId>ch.qos.logback</groupId>
@ -112,6 +102,12 @@
<groupId>me.liuwj.ktorm</groupId> <groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId> <artifactId>ktorm-core</artifactId>
</dependency> </dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-views</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies> </dependencies>
<dependencyManagement> <dependencyManagement>

View File

@ -6,6 +6,7 @@ import be.simplenotes.domain.domainModule
import be.simplenotes.persistance.migrationModule import be.simplenotes.persistance.migrationModule
import be.simplenotes.persistance.persistanceModule import be.simplenotes.persistance.persistanceModule
import be.simplenotes.search.searchModule import be.simplenotes.search.searchModule
import be.simplenotes.views.viewModule
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.context.unloadKoinModules import org.koin.core.context.unloadKoinModules
@ -16,10 +17,8 @@ fun main() {
persistanceModule, persistanceModule,
migrationModule, migrationModule,
configModule, configModule,
baseModule, viewModule,
userModule, controllerModule,
noteModule,
settingsModule,
domainModule, domainModule,
searchModule, searchModule,
apiModule, apiModule,

View File

@ -4,8 +4,8 @@ import be.simplenotes.app.extensions.auto
import be.simplenotes.app.utils.parseSearchTerms import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.NoteService import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.types.LoggedInUser
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -20,27 +20,27 @@ import java.util.*
class ApiNoteController(private val noteService: NoteService, private val json: Json) { 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) val content = noteContentLens(request)
return noteService.create(jwtPayload.userId, content).fold( return noteService.create(loggedInUser.userId, content).fold(
{ Response(BAD_REQUEST) }, { Response(BAD_REQUEST) },
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) } { uuidContentLens(UuidContent(it.uuid), Response(OK)) }
) )
} }
fun notes(request: Request, jwtPayload: JwtPayload): Response { fun notes(request: Request, loggedInUser: LoggedInUser): Response {
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes val notes = noteService.paginatedNotes(loggedInUser.userId, page = 1).notes
return persistedNotesMetadataLens(notes, Response(OK)) return persistedNotesMetadataLens(notes, Response(OK))
} }
fun note(request: Request, jwtPayload: JwtPayload): Response = fun note(request: Request, loggedInUser: LoggedInUser): Response =
noteService.find(jwtPayload.userId, uuidLens(request)) noteService.find(loggedInUser.userId, uuidLens(request))
?.let { persistedNoteLens(it, Response(OK)) } ?.let { persistedNoteLens(it, Response(OK)) }
?: Response(NOT_FOUND) ?: Response(NOT_FOUND)
fun update(request: Request, jwtPayload: JwtPayload): Response { fun update(request: Request, loggedInUser: LoggedInUser): Response {
val content = noteContentLens(request) 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) Response(BAD_REQUEST)
}, { }, {
if (it == null) Response(NOT_FOUND) 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 query = searchContentLens(request)
val terms = parseSearchTerms(query) val terms = parseSearchTerms(query)
val notes = noteService.search(jwtPayload.userId, terms) val notes = noteService.search(loggedInUser.userId, terms)
return persistedNotesMetadataLens(notes, Response(OK)) return persistedNotesMetadataLens(notes, Response(OK))
} }

View File

@ -1,13 +1,13 @@
package be.simplenotes.app.controllers package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.BaseView import be.simplenotes.types.LoggedInUser
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.views.BaseView
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
class BaseController(private val view: BaseView) { class BaseController(private val view: BaseView) {
fun index(@Suppress("UNUSED_PARAMETER") request: Request, jwtPayload: JwtPayload?) = fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) =
Response(OK).html(view.renderHome(jwtPayload)) Response(OK).html(view.renderHome(loggedInUser))
} }

View File

@ -3,12 +3,12 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect import be.simplenotes.app.extensions.redirect
import be.simplenotes.app.utils.parseSearchTerms import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.app.views.NoteView import be.simplenotes.views.NoteView
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.NoteService import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.usecases.markdown.InvalidMeta import be.simplenotes.domain.usecases.markdown.InvalidMeta
import be.simplenotes.domain.usecases.markdown.MissingMeta import be.simplenotes.domain.usecases.markdown.MissingMeta
import be.simplenotes.domain.usecases.markdown.ValidationError import be.simplenotes.domain.usecases.markdown.ValidationError
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Method import org.http4k.core.Method
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
@ -25,18 +25,18 @@ class NoteController(
private val noteService: NoteService, private val noteService: NoteService,
) { ) {
fun new(request: Request, jwtPayload: JwtPayload): Response { fun new(request: Request, loggedInUser: LoggedInUser): Response {
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(jwtPayload)) if (request.method == Method.GET) return Response(OK).html(view.noteEditor(loggedInUser))
val markdownForm = request.form("markdown") ?: "" val markdownForm = request.form("markdown") ?: ""
return noteService.create(jwtPayload.userId, markdownForm).fold( return noteService.create(loggedInUser.userId, markdownForm).fold(
{ {
val html = when (it) { val html = when (it) {
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm) MissingMeta -> view.noteEditor(loggedInUser, error = "Missing note metadata", textarea = markdownForm)
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm) InvalidMeta -> view.noteEditor(loggedInUser, error = "Invalid note metadata", textarea = markdownForm)
is ValidationError -> view.noteEditor( is ValidationError -> view.noteEditor(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors, validationErrors = it.validationErrors,
textarea = markdownForm 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 currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
val tag = request.query("tag") val tag = request.query("tag")
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag) val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag)
val deletedCount = noteService.countDeleted(jwtPayload.userId) val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages, deletedCount, tag = tag)) 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 query = request.form("search") ?: ""
val terms = parseSearchTerms(query) val terms = parseSearchTerms(query)
val notes = noteService.search(jwtPayload.userId, terms) val notes = noteService.search(loggedInUser.userId, terms)
val deletedCount = noteService.countDeleted(jwtPayload.userId) val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.search(jwtPayload, notes, query, deletedCount)) 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) val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
if (request.method == Method.POST) { if (request.method == Method.POST) {
if (request.form("delete") != null) { 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 ? Response.redirect("/notes") // TODO: flash cookie to show success ?
else else
Response(NOT_FOUND) // TODO: show an error Response(NOT_FOUND) // TODO: show an error
} }
if (request.form("public") != null) { 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) { } 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) val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
return Response(OK).html(view.renderedNote(jwtPayload, note, shared = false)) 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 noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
val note = noteService.findPublic(noteUuid) ?: 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 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) { 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") ?: "" 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) { val html = when (it) {
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm) MissingMeta -> view.noteEditor(loggedInUser, error = "Missing note metadata", textarea = markdownForm)
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm) InvalidMeta -> view.noteEditor(loggedInUser, error = "Invalid note metadata", textarea = markdownForm)
is ValidationError -> view.noteEditor( is ValidationError -> view.noteEditor(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors, validationErrors = it.validationErrors,
textarea = markdownForm 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 currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
val tag = request.query("tag") val tag = request.query("tag")
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag, deleted = true) val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag, deleted = true)
return Response(OK).html(view.trash(jwtPayload, notes, currentPage, pages)) 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) val uuid = request.uuidPath() ?: return Response(NOT_FOUND)
return if (request.form("delete") != null) return if (request.form("delete") != null)
if (noteService.delete(jwtPayload.userId, uuid)) if (noteService.delete(loggedInUser.userId, uuid))
Response.redirect("/notes/trash") Response.redirect("/notes/trash")
else else
Response(NOT_FOUND) Response(NOT_FOUND)
else if (noteService.restore(jwtPayload.userId, uuid)) else if (noteService.restore(loggedInUser.userId, uuid))
Response.redirect("/notes/$uuid") Response.redirect("/notes/$uuid")
else else
Response(NOT_FOUND) Response(NOT_FOUND)

View File

@ -2,11 +2,11 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect import be.simplenotes.app.extensions.redirect
import be.simplenotes.app.views.SettingView import be.simplenotes.views.SettingView
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.UserService import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.delete.DeleteError import be.simplenotes.domain.usecases.users.delete.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm import be.simplenotes.domain.usecases.users.delete.DeleteForm
import be.simplenotes.types.LoggedInUser
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.body.form import org.http4k.core.body.form
import org.http4k.core.cookie.invalidateCookie import org.http4k.core.cookie.invalidateCookie
@ -15,11 +15,11 @@ class SettingsController(
private val userService: UserService, private val userService: UserService,
private val settingView: SettingView, private val settingView: SettingView,
) { ) {
fun settings(request: Request, jwtPayload: JwtPayload): Response { fun settings(request: Request, loggedInUser: LoggedInUser): Response {
if (request.method == Method.GET) 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) val result = userService.delete(deleteForm)
return result.fold( return result.fold(
@ -28,13 +28,13 @@ class SettingsController(
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer") DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer")
DeleteError.WrongPassword -> Response(Status.OK).html( DeleteError.WrongPassword -> Response(Status.OK).html(
settingView.settings( settingView.settings(
jwtPayload, loggedInUser,
error = "Wrong password" error = "Wrong password"
) )
) )
is DeleteError.InvalidForm -> Response(Status.OK).html( is DeleteError.InvalidForm -> Response(Status.OK).html(
settingView.settings( settingView.settings(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors
) )
) )
@ -53,23 +53,23 @@ class SettingsController(
.header("Content-Type", contentType) .header("Content-Type", contentType)
} }
fun export(request: Request, jwtPayload: JwtPayload): Response { fun export(request: Request, loggedInUser: LoggedInUser): Response {
val isDownload = request.form("download") != null val isDownload = request.form("download") != null
return if (isDownload) { return if (isDownload) {
val filename = "simplenotes-export-${jwtPayload.username}" val filename = "simplenotes-export-${loggedInUser.username}"
if (request.form("format") == "zip") { if (request.form("format") == "zip") {
val zip = userService.exportAsZip(jwtPayload.userId) val zip = userService.exportAsZip(loggedInUser.userId)
Response(Status.OK) Response(Status.OK)
.with(attachment("$filename.zip", "application/zip")) .with(attachment("$filename.zip", "application/zip"))
.body(zip) .body(zip)
} else } else
Response(Status.OK) Response(Status.OK)
.with(attachment("$filename.json", "application/json")) .with(attachment("$filename.json", "application/json"))
.body(userService.exportAsJson(jwtPayload.userId)) .body(userService.exportAsJson(loggedInUser.userId))
} else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json") } else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header("Content-Type", "application/json")
} }
private fun Request.deleteForm(jwtPayload: JwtPayload) = private fun Request.deleteForm(loggedInUser: LoggedInUser) =
DeleteForm(jwtPayload.username, form("password"), form("checked") != null) DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
} }

View File

@ -3,14 +3,14 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.isSecure import be.simplenotes.app.extensions.isSecure
import be.simplenotes.app.extensions.redirect import be.simplenotes.app.extensions.redirect
import be.simplenotes.app.views.UserView import be.simplenotes.views.UserView
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.UserService import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.* import be.simplenotes.domain.usecases.users.login.*
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
import be.simplenotes.domain.usecases.users.register.RegisterForm import be.simplenotes.domain.usecases.users.register.RegisterForm
import be.simplenotes.domain.usecases.users.register.UserExists import be.simplenotes.domain.usecases.users.register.UserExists
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
@ -27,9 +27,9 @@ class UserController(
private val userView: UserView, private val userView: UserView,
private val jwtConfig: JwtConfig, 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( if (request.method == GET) return Response(OK).html(
userView.register(jwtPayload) userView.register(loggedInUser)
) )
val result = userService.register(request.registerForm()) val result = userService.register(request.registerForm())
@ -38,12 +38,12 @@ class UserController(
{ {
val html = when (it) { val html = when (it) {
UserExists -> userView.register( UserExists -> userView.register(
jwtPayload, loggedInUser,
error = "User already exists" error = "User already exists"
) )
is InvalidRegisterForm -> is InvalidRegisterForm ->
userView.register( userView.register(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors
) )
} }
@ -58,9 +58,9 @@ class UserController(
private fun Request.registerForm() = RegisterForm(form("username"), form("password")) private fun Request.registerForm() = RegisterForm(form("username"), form("password"))
private fun Request.loginForm(): LoginForm = registerForm() 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( if (request.method == GET) return Response(OK).html(
userView.login(jwtPayload) userView.login(loggedInUser)
) )
val result = userService.login(request.loginForm()) val result = userService.login(request.loginForm())
@ -70,17 +70,17 @@ class UserController(
val html = when (it) { val html = when (it) {
Unregistered -> Unregistered ->
userView.login( userView.login(
jwtPayload, loggedInUser,
error = "User does not exist" error = "User does not exist"
) )
WrongPassword -> WrongPassword ->
userView.login( userView.login(
jwtPayload, loggedInUser,
error = "Wrong password" error = "Wrong password"
) )
is InvalidLoginForm -> is InvalidLoginForm ->
userView.login( userView.login(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors
) )
} }

View File

@ -1,8 +1,8 @@
package be.simplenotes.app.filters package be.simplenotes.app.filters
import be.simplenotes.app.extensions.redirect import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.JwtPayloadExtractor import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.types.LoggedInUser
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Status.Companion.UNAUTHORIZED import org.http4k.core.Status.Companion.UNAUTHORIZED
import org.http4k.core.cookie.cookie 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 { enum class JwtSource {
Header, Cookie Header, Cookie

View File

@ -1,8 +1,8 @@
package be.simplenotes.app.filters package be.simplenotes.app.filters
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.ErrorView import be.simplenotes.views.ErrorView
import be.simplenotes.app.views.ErrorView.Type.* import be.simplenotes.views.ErrorView.Type.*
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
import org.http4k.core.Status.Companion.NOT_FOUND import org.http4k.core.Status.Companion.NOT_FOUND

View File

@ -1,6 +1,8 @@
package be.simplenotes.app.modules package be.simplenotes.app.modules
import be.simplenotes.app.Config import be.simplenotes.app.Config
import be.simplenotes.app.utils.StaticFileResolver
import org.koin.core.qualifier.named
import org.koin.dsl.module import org.koin.dsl.module
val configModule = module { val configModule = module {

View File

@ -1,29 +1,12 @@
package be.simplenotes.app.modules package be.simplenotes.app.modules
import be.simplenotes.app.controllers.* 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 import org.koin.dsl.module
val userModule = module { val controllerModule = module {
single { UserController(get(), get(), get()) } single { UserController(get(), get(), get()) }
single { UserView(get()) }
}
val baseModule = module {
single { HealthCheckController(get()) } single { HealthCheckController(get()) }
single { BaseController(get()) } single { BaseController(get()) }
single { BaseView(get()) }
}
val noteModule = module {
single { NoteController(get(), get()) } single { NoteController(get(), get()) }
single { NoteView(get()) }
}
val settingsModule = module {
single { SettingsController(get(), get()) } single { SettingsController(get(), get()) }
single { SettingView(get()) }
} }

View File

@ -10,7 +10,7 @@ import be.simplenotes.app.jetty.Jetty
import be.simplenotes.app.routes.Router import be.simplenotes.app.routes.Router
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.utils.StaticFileResolverImpl import be.simplenotes.app.utils.StaticFileResolverImpl
import be.simplenotes.app.views.ErrorView import be.simplenotes.views.ErrorView
import be.simplenotes.config.ServerConfig import be.simplenotes.config.ServerConfig
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import org.http4k.core.Filter import org.http4k.core.Filter
@ -59,5 +59,5 @@ val serverModule = module {
single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) } single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) }
single { ErrorFilter(get()) } single { ErrorFilter(get()) }
single { TransactionFilter(get()) } single { TransactionFilter(get()) }
single { ErrorView(get()) } single(named("styles")) { get<StaticFileResolver>().resolve("styles.css") }
} }

View File

@ -4,7 +4,7 @@ import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.controllers.* import be.simplenotes.app.controllers.*
import be.simplenotes.app.filters.* import be.simplenotes.app.filters.*
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.types.LoggedInUser
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Method.* import org.http4k.core.Method.*
import org.http4k.filter.ResponseFilters.GZip import org.http4k.filter.ResponseFilters.GZip
@ -102,5 +102,5 @@ class Router(
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) } this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) }
} }
private typealias PublicHandler = (Request, JwtPayload?) -> Response private typealias PublicHandler = (Request, LoggedInUser?) -> Response
private typealias ProtectedHandler = (Request, JwtPayload) -> Response private typealias ProtectedHandler = (Request, LoggedInUser) -> Response

View File

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

View File

@ -1,9 +1,9 @@
package be.simplenotes.app.filters package be.simplenotes.app.filters
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.JwtPayloadExtractor import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.types.LoggedInUser
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
@ -58,7 +58,7 @@ internal class AuthFilterTest {
@Test @Test
fun `it should allow a valid token`() { fun `it should allow a valid token`() {
val jwtPayload = JwtPayload(1, "user") val jwtPayload = LoggedInUser(1, "user")
val token = simpleJwt.sign(jwtPayload) val token = simpleJwt.sign(jwtPayload)
val response = app(Request(GET, "/optional").cookie("Bearer", token)) val response = app(Request(GET, "/optional").cookie("Bearer", token))
assertThat(response, hasStatus(OK)) assertThat(response, hasStatus(OK))
@ -84,7 +84,7 @@ internal class AuthFilterTest {
@Test @Test
fun `it should allow a valid token"`() { fun `it should allow a valid token"`() {
val jwtPayload = JwtPayload(1, "user") val jwtPayload = LoggedInUser(1, "user")
val token = simpleJwt.sign(jwtPayload) val token = simpleJwt.sign(jwtPayload)
val response = app(Request(GET, "/protected").cookie("Bearer", token)) val response = app(Request(GET, "/protected").cookie("Bearer", token))
assertThat(response, hasStatus(OK)) assertThat(response, hasStatus(OK))

View File

@ -1,18 +1,14 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.types.PersistedUser import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.exceptions.JWTVerificationException 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) { 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 decodedJWT = jwt.verifier.verify(token)
val id = decodedJWT.getClaim(userIdField).asInt() ?: null val id = decodedJWT.getClaim(userIdField).asInt() ?: null
val username = decodedJWT.getClaim(usernameField).asString() ?: 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) { } catch (e: JWTVerificationException) {
null null
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {

View File

@ -1,6 +1,7 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
@ -15,9 +16,9 @@ class SimpleJwt(jwtConfig: JwtConfig) {
private val algorithm = Algorithm.HMAC256(jwtConfig.secret) private val algorithm = Algorithm.HMAC256(jwtConfig.secret)
val verifier: JWTVerifier = JWT.require(algorithm).build() val verifier: JWTVerifier = JWT.require(algorithm).build()
fun sign(jwtPayload: JwtPayload): String = JWT.create() fun sign(loggedInUser: LoggedInUser): String = JWT.create()
.withClaim(userIdField, jwtPayload.userId) .withClaim(userIdField, loggedInUser.userId)
.withClaim(usernameField, jwtPayload.username) .withClaim(usernameField, loggedInUser.username)
.withExpiresAt(getExpiration()) .withExpiresAt(getExpiration())
.sign(algorithm) .sign(algorithm)

View File

@ -4,11 +4,11 @@ import arrow.core.Either
import arrow.core.extensions.fx import arrow.core.extensions.fx
import arrow.core.filterOrElse import arrow.core.filterOrElse
import arrow.core.rightIfNotNull import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.types.LoggedInUser
internal class LoginUseCaseImpl( internal class LoginUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
@ -20,6 +20,6 @@ internal class LoginUseCaseImpl(
!userRepository.find(user.username) !userRepository.find(user.username)
.rightIfNotNull { Unregistered } .rightIfNotNull { Unregistered }
.filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword }) .filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword })
.map { jwt.sign(JwtPayload(it)) } .map { jwt.sign(LoggedInUser(it)) }
} }
} }

View File

@ -2,6 +2,7 @@ package be.simplenotes.domain.security
import be.simplenotes.domain.usecases.users.login.Token import be.simplenotes.domain.usecases.users.login.Token
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import com.natpryce.hamkrest.absent import com.natpryce.hamkrest.absent
@ -13,7 +14,7 @@ import org.junit.jupiter.params.provider.MethodSource
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.stream.Stream import java.util.stream.Stream
internal class JwtPayloadExtractorTest { internal class LoggedInUserExtractorTest {
private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS) private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
private val simpleJwt = SimpleJwt(jwtConfig) private val simpleJwt = SimpleJwt(jwtConfig)
private val jwtPayloadExtractor = JwtPayloadExtractor(simpleJwt) private val jwtPayloadExtractor = JwtPayloadExtractor(simpleJwt)
@ -45,6 +46,6 @@ internal class JwtPayloadExtractorTest {
@Test @Test
fun `parse valid token`() { fun `parse valid token`() {
val token = createToken(username = "someone", id = 1) val token = createToken(username = "someone", id = 1)
assertThat(jwtPayloadExtractor(token), equalTo(JwtPayload(1, "someone"))) assertThat(jwtPayloadExtractor(token), equalTo(LoggedInUser(1, "someone")))
} }
} }

View File

@ -2,3 +2,6 @@ package be.simplenotes.types
data class User(val username: String, val password: String) data class User(val username: String, val password: String)
data class PersistedUser(val username: String, val password: String, val id: Int) 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)
}

41
simplenotes-views/pom.xml Normal file
View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simplenotes-parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-views</artifactId>
<dependencies>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
</dependency>
<dependency>
<groupId>io.konform</groupId>
<artifactId>konform-jvm</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-html-jvm</artifactId>
<version>0.7.1</version>
</dependency>
<dependency>
<groupId>org.ocpsoft.prettytime</groupId>
<artifactId>prettytime</artifactId>
<version>4.0.5.Final</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-types</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>
</project>

View File

@ -1,15 +1,14 @@
package be.simplenotes.app.views package be.simplenotes.views
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.types.LoggedInUser
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ThScope.col import kotlinx.html.ThScope.col
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { class BaseView(styles: String) : View(styles) {
fun renderHome(jwtPayload: JwtPayload?) = renderPage( fun renderHome(loggedInUser: LoggedInUser?) = renderPage(
title = "Home", title = "Home",
description = "A fast and simple note taking website", description = "A fast and simple note taking website",
jwtPayload = jwtPayload loggedInUser = loggedInUser
) { ) {
section("text-center my-2 p-2") { section("text-center my-2 p-2") {
h1("text-5xl casual") { h1("text-5xl casual") {

View File

@ -1,12 +1,11 @@
package be.simplenotes.app.views package be.simplenotes.views
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.views.components.Alert
import be.simplenotes.app.views.components.Alert import be.simplenotes.views.components.alert
import be.simplenotes.app.views.components.alert
import kotlinx.html.a import kotlinx.html.a
import kotlinx.html.div import kotlinx.html.div
class ErrorView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { class ErrorView(styles: String) : View(styles) {
enum class Type(val title: String) { enum class Type(val title: String) {
SqlTransientError("Database unavailable"), SqlTransientError("Database unavailable"),
@ -14,7 +13,7 @@ class ErrorView(staticFileResolver: StaticFileResolver) : View(staticFileResolve
Other("Error"), 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") { div("container mx-auto p-4") {
when (errorType) { when (errorType) {
Type.SqlTransientError -> alert( Type.SqlTransientError -> alert(

View File

@ -1,21 +1,21 @@
package be.simplenotes.app.views package be.simplenotes.views
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.types.LoggedInUser
import be.simplenotes.app.views.components.* import be.simplenotes.views.components.noteListHeader
import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.views.components.*
import io.konform.validation.ValidationError import io.konform.validation.ValidationError
import kotlinx.html.* import kotlinx.html.*
class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { class NoteView(styles: String) : View(styles) {
fun noteEditor( fun noteEditor(
jwtPayload: JwtPayload, loggedInUser: LoggedInUser,
error: String? = null, error: String? = null,
textarea: String? = null, textarea: String? = null,
validationErrors: List<ValidationError> = emptyList(), validationErrors: List<ValidationError> = emptyList(),
) = renderPage(title = "New note", jwtPayload = jwtPayload) { ) = renderPage(title = "New note", loggedInUser = loggedInUser) {
div("container mx-auto p-4") { div("container mx-auto p-4") {
error?.let { alert(Alert.Warning, error) } error?.let { alert(Alert.Warning, error) }
validationErrors.forEach { validationErrors.forEach {
@ -46,13 +46,13 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
fun notes( fun notes(
jwtPayload: JwtPayload, loggedInUser: LoggedInUser,
notes: List<PersistedNoteMetadata>, notes: List<PersistedNoteMetadata>,
currentPage: Int, currentPage: Int,
numberOfPages: Int, numberOfPages: Int,
numberOfDeletedNotes: Int, numberOfDeletedNotes: Int,
tag: String?, tag: String?,
) = renderPage(title = "Notes", jwtPayload = jwtPayload) { ) = renderPage(title = "Notes", loggedInUser = loggedInUser) {
div("container mx-auto p-4") { div("container mx-auto p-4") {
noteListHeader(numberOfDeletedNotes) noteListHeader(numberOfDeletedNotes)
if (notes.isNotEmpty()) if (notes.isNotEmpty())
@ -68,11 +68,11 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
fun search( fun search(
jwtPayload: JwtPayload, loggedInUser: LoggedInUser,
notes: List<PersistedNoteMetadata>, notes: List<PersistedNoteMetadata>,
query: String, query: String,
numberOfDeletedNotes: Int, numberOfDeletedNotes: Int,
) = renderPage("Notes", jwtPayload = jwtPayload) { ) = renderPage("Notes", loggedInUser = loggedInUser) {
div("container mx-auto p-4") { div("container mx-auto p-4") {
noteListHeader(numberOfDeletedNotes, query) noteListHeader(numberOfDeletedNotes, query)
noteTable(notes) noteTable(notes)
@ -80,11 +80,11 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
fun trash( fun trash(
jwtPayload: JwtPayload, loggedInUser: LoggedInUser,
notes: List<PersistedNoteMetadata>, notes: List<PersistedNoteMetadata>,
currentPage: Int, currentPage: Int,
numberOfPages: Int, numberOfPages: Int,
) = renderPage(title = "Notes", jwtPayload = jwtPayload) { ) = renderPage(title = "Notes", loggedInUser = loggedInUser) {
div("container mx-auto p-4") { div("container mx-auto p-4") {
div("flex justify-between mb-4") { div("flex justify-between mb-4") {
h1("text-2xl underline") { +"Deleted notes" } 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, note.meta.title,
jwtPayload = jwtPayload, loggedInUser = loggedInUser,
scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js") scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js")
) { ) {
div("container mx-auto p-4") { div("container mx-auto p-4") {

View File

@ -1,28 +1,27 @@
package be.simplenotes.app.views package be.simplenotes.views
import be.simplenotes.app.extensions.summary import be.simplenotes.types.LoggedInUser
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.views.components.Alert
import be.simplenotes.app.views.components.Alert import be.simplenotes.views.components.alert
import be.simplenotes.app.views.components.alert import be.simplenotes.views.components.input
import be.simplenotes.app.views.components.input import be.simplenotes.views.extensions.summary
import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError import io.konform.validation.ValidationError
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ButtonType.submit import kotlinx.html.ButtonType.submit
class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { class SettingView(styles: String) : View(styles) {
fun settings( fun settings(
jwtPayload: JwtPayload, loggedInUser: LoggedInUser,
error: String? = null, error: String? = null,
validationErrors: List<ValidationError> = emptyList(), validationErrors: List<ValidationError> = emptyList(),
) = renderPage("Settings", jwtPayload = jwtPayload) { ) = renderPage("Settings", loggedInUser = loggedInUser) {
div("container mx-auto") { div("container mx-auto") {
section("m-4 p-4 bg-gray-800 rounded") { section("m-4 p-4 bg-gray-800 rounded") {
h1("text-xl") { h1("text-xl") {
+"Welcome " +"Welcome "
span("text-teal-200 font-semibold") { +jwtPayload.username } span("text-teal-200 font-semibold") { +loggedInUser.username }
} }
} }

View File

@ -1,23 +1,22 @@
package be.simplenotes.app.views package be.simplenotes.views
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.types.LoggedInUser
import be.simplenotes.app.views.components.Alert import be.simplenotes.views.components.Alert
import be.simplenotes.app.views.components.alert import be.simplenotes.views.components.alert
import be.simplenotes.app.views.components.input import be.simplenotes.views.components.input
import be.simplenotes.app.views.components.submitButton import be.simplenotes.views.components.submitButton
import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError import io.konform.validation.ValidationError
import kotlinx.html.* import kotlinx.html.*
class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) { class UserView(styles: String) : View(styles) {
fun register( fun register(
jwtPayload: JwtPayload?, loggedInUser: LoggedInUser?,
error: String? = null, error: String? = null,
validationErrors: List<ValidationError> = emptyList(), validationErrors: List<ValidationError> = emptyList(),
) = accountForm( ) = accountForm(
"Register", "Register",
"Registration page", "Registration page",
jwtPayload, loggedInUser,
error, error,
validationErrors, validationErrors,
"Create an account", "Create an account",
@ -28,11 +27,11 @@ class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
} }
fun login( fun login(
jwtPayload: JwtPayload?, loggedInUser: LoggedInUser?,
error: String? = null, error: String? = null,
validationErrors: List<ValidationError> = emptyList(), validationErrors: List<ValidationError> = emptyList(),
new: Boolean = false, 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? " +"Don't have an account yet? "
a(href = "/register", classes = "no-underline text-blue-500 hover:text-blue-400 font-bold") { a(href = "/register", classes = "no-underline text-blue-500 hover:text-blue-400 font-bold") {
+"Create an account" +"Create an account"
@ -42,14 +41,14 @@ class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
private fun accountForm( private fun accountForm(
title: String, title: String,
description: String, description: String,
jwtPayload: JwtPayload?, loggedInUser: LoggedInUser?,
error: String? = null, error: String? = null,
validationErrors: List<ValidationError> = emptyList(), validationErrors: List<ValidationError> = emptyList(),
h1: String, h1: String,
submit: String, submit: String,
new: Boolean = false, new: Boolean = false,
footer: FlowContent.() -> Unit, 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("centered container mx-auto flex justify-center items-center") {
div("w-full md:w-1/2 lg:w-1/3 m-4") { div("w-full md:w-1/2 lg:w-1/3 m-4") {
div("p-8 mb-6") { div("p-8 mb-6") {

View File

@ -1,19 +1,16 @@
package be.simplenotes.app.views package be.simplenotes.views
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.types.LoggedInUser
import be.simplenotes.app.views.components.navbar import be.simplenotes.views.components.navbar
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.stream.appendHTML import kotlinx.html.stream.appendHTML
abstract class View(staticFileResolver: StaticFileResolver) { abstract class View(private val styles: String) {
private val styles = staticFileResolver.resolve("styles.css")!!
fun renderPage( fun renderPage(
title: String, title: String,
description: String? = null, description: String? = null,
jwtPayload: JwtPayload?, loggedInUser: LoggedInUser?,
scripts: List<String> = emptyList(), scripts: List<String> = emptyList(),
body: MAIN.() -> Unit = {}, body: MAIN.() -> Unit = {},
) = buildString { ) = buildString {
@ -37,7 +34,7 @@ abstract class View(staticFileResolver: StaticFileResolver) {
} }
} }
body("bg-gray-900 text-white") { body("bg-gray-900 text-white") {
navbar(jwtPayload) navbar(loggedInUser)
main { body() } main { body() }
} }
} }

View File

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

View File

@ -1,8 +1,8 @@
package be.simplenotes.app.views.components package be.simplenotes.views.components
import kotlinx.html.* 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) { val colors = when (type) {
Alert.Success -> "bg-green-500 border border-green-400 text-gray-800" Alert.Success -> "bg-green-500 border border-green-400 text-gray-800"
Alert.Warning -> "bg-red-500 border border-red-400 text-red-200" 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 Success, Warning
} }

View File

@ -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.types.PersistedNoteMetadata
import be.simplenotes.views.utils.toTimeAgo
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ButtonType.submit import kotlinx.html.ButtonType.submit
import kotlinx.html.FormMethod.post import kotlinx.html.FormMethod.post
import kotlinx.html.ThScope.col import kotlinx.html.ThScope.col
fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") { internal fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") {
table { table {
id = "notes" id = "notes"
thead { thead {

View File

@ -1,9 +1,9 @@
package be.simplenotes.app.views.components package be.simplenotes.views.components
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ButtonType.submit import kotlinx.html.ButtonType.submit
fun FlowContent.input( internal fun FlowContent.input(
type: InputType = InputType.text, type: InputType = InputType.text,
placeholder: String, placeholder: String,
id: 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") { div("flex items-center mt-6") {
button( button(
type = submit, type = submit,

View File

@ -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.* import kotlinx.html.*
fun BODY.navbar(jwtPayload: JwtPayload?) { internal fun BODY.navbar(loggedInUser: LoggedInUser?) {
nav { nav {
id = "navbar" id = "navbar"
a("/") { a("/") {
@ -12,7 +12,7 @@ fun BODY.navbar(jwtPayload: JwtPayload?) {
} }
ul("space-x-2") { ul("space-x-2") {
id = "navigation" id = "navigation"
if (jwtPayload != null) { if (loggedInUser != null) {
val links = listOf( val links = listOf(
"/notes" to "Notes", "/notes" to "Notes",
"/settings" to "Settings", "/settings" to "Settings",

View File

@ -1,10 +1,10 @@
package be.simplenotes.app.views.components package be.simplenotes.views.components
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ButtonType.submit import kotlinx.html.ButtonType.submit
import kotlinx.html.FormMethod.post 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") { div("flex justify-between mb-4") {
h1("text-2xl underline") { +"Notes" } h1("text-2xl underline") { +"Notes" }
span { span {

View File

@ -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.types.PersistedNoteMetadata
import be.simplenotes.views.utils.toTimeAgo
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ThScope.col import kotlinx.html.ThScope.col
fun FlowContent.noteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") { internal fun FlowContent.noteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") {
table { table {
id = "notes" id = "notes"
thead { thead {

View File

@ -1,8 +1,8 @@
package be.simplenotes.app.extensions package be.simplenotes.views.extensions
import kotlinx.html.* import kotlinx.html.*
class SUMMARY(consumer: TagConsumer<*>) : internal class SUMMARY(consumer: TagConsumer<*>) :
HTMLTag( HTMLTag(
"summary", consumer, emptyMap(), "summary", consumer, emptyMap(),
inlineTag = true, inlineTag = true,
@ -10,6 +10,6 @@ class SUMMARY(consumer: TagConsumer<*>) :
), ),
HtmlInlineTag HtmlInlineTag
fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) { internal fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) {
SUMMARY(consumer).visit(block) SUMMARY(consumer).visit(block)
} }

View File

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