From c5f9a1d6e0dffa4dc11278a1dd93086f0d11c3be Mon Sep 17 00:00:00 2001 From: Hubert Van De Walle Date: Tue, 25 Aug 2020 06:18:18 +0200 Subject: [PATCH] Add possibility to share notes --- .../main/kotlin/controllers/NoteController.kt | 25 +++++-- app/src/main/kotlin/routes/Router.kt | 1 + app/src/main/kotlin/views/NoteView.kt | 69 +++++++++++++++---- domain/src/main/kotlin/model/Note.kt | 1 + .../src/main/kotlin/usecases/NoteService.kt | 4 ++ .../usecases/repositories/NoteRepository.kt | 4 ++ .../main/kotlin/notes/NoteRepositoryImpl.kt | 29 +++++++- persistance/src/main/kotlin/notes/Notes.kt | 5 +- .../mariadb/V3__Add_public_column.sql | 2 + .../migration/other/V3__Add_public_column.sql | 2 + .../src/test/kotlin/NoteSearcherImplTest.kt | 7 +- 11 files changed, 128 insertions(+), 21 deletions(-) create mode 100644 persistance/src/main/resources/db/migration/mariadb/V3__Add_public_column.sql create mode 100644 persistance/src/main/resources/db/migration/other/V3__Add_public_column.sql diff --git a/app/src/main/kotlin/controllers/NoteController.kt b/app/src/main/kotlin/controllers/NoteController.kt index db0dde6..9c3d326 100644 --- a/app/src/main/kotlin/controllers/NoteController.kt +++ b/app/src/main/kotlin/controllers/NoteController.kt @@ -68,15 +68,28 @@ class NoteController( fun note(request: Request, jwtPayload: JwtPayload): Response { val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND) - if (request.method == Method.POST && request.form("delete") != null) { - return if (noteService.trash(jwtPayload.userId, noteUuid)) - Response.redirect("/notes") // TODO: flash cookie to show success ? - else - Response(NOT_FOUND) // TODO: show an error + if (request.method == Method.POST) { + if (request.form("delete") != null) { + return if (noteService.trash(jwtPayload.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) + } else if (request.form("private") != null) { + if (!noteService.makePrivate(jwtPayload.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)) + return Response(OK).html(view.renderedNote(jwtPayload, note, shared = false)) + } + + fun public(request: Request, jwtPayload: JwtPayload?): 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)) } fun edit(request: Request, jwtPayload: JwtPayload): Response { diff --git a/app/src/main/kotlin/routes/Router.kt b/app/src/main/kotlin/routes/Router.kt index 9c7cd85..3c6e0a6 100644 --- a/app/src/main/kotlin/routes/Router.kt +++ b/app/src/main/kotlin/routes/Router.kt @@ -42,6 +42,7 @@ class Router( "/login" bind GET public userController::login, "/login" bind POST public userController::login, "/logout" bind POST to userController::logout, + "/notes/public/{uuid}" bind GET public noteController::public, ) val protectedRoutes = routes( diff --git a/app/src/main/kotlin/views/NoteView.kt b/app/src/main/kotlin/views/NoteView.kt index dbf5310..a06eb1e 100644 --- a/app/src/main/kotlin/views/NoteView.kt +++ b/app/src/main/kotlin/views/NoteView.kt @@ -116,12 +116,21 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } } - fun renderedNote(jwtPayload: JwtPayload, note: PersistedNote) = renderPage( + fun renderedNote(jwtPayload: JwtPayload?, note: PersistedNote, shared: Boolean) = renderPage( note.meta.title, jwtPayload = jwtPayload, scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js") ) { div("container mx-auto p-4") { + + if (shared) { + p("p-4 bg-gray-800") { + +"You are viewing a public note " + } + + hr { } + } + div("flex items-center justify-between mb-4") { h1("text-3xl fond-bold underline") { +note.meta.title } span("space-x-2") { @@ -132,19 +141,21 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } } } - span("flex space-x-2 justify-end mb-4") { - a( - href = "/notes/${note.uuid}/edit", - classes = "btn btn-teal" - ) { +"Edit" } - form(method = FormMethod.post, classes = "inline") { - button( - type = ButtonType.submit, - name = "delete", - classes = "btn btn-red" - ) { +"Delete" } + if (!shared) { + noteActionForm(note) + publicPrivateForm(note) + + if (note.public) { + p("my-4") { + +"You can share this link : " + a(href = "/notes/public/${note.uuid}", classes = "text-blue-300 underline") { + +"/notes/public/${note.uuid}" + } + } + hr { } } } + div { attributes["id"] = "note" unsafe { @@ -153,4 +164,38 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver } } } + + private fun DIV.noteActionForm(note: PersistedNote) { + span("flex space-x-2 justify-end mb-4") { + a( + href = "/notes/${note.uuid}/edit", + classes = "btn btn-teal" + ) { +"Edit" } + form(method = FormMethod.post, classes = "inline") { + button( + type = ButtonType.submit, + name = "delete", + classes = "btn btn-red" + ) { +"Delete" } + } + } + } + + private fun DIV.publicPrivateForm(note: PersistedNote) { + span("flex space-x-2 justify-end mb-4") { + + form(method = FormMethod.post, classes = "ml-auto ") { + button( + type = ButtonType.submit, + name = if (note.public) "private" else "public", + classes = "btn btn-teal" + ) { + if (note.public) + +"This note is public, do you want to make it private ?" + else + +"This note is private, do you want to make it public ?" + } + } + } + } } diff --git a/domain/src/main/kotlin/model/Note.kt b/domain/src/main/kotlin/model/Note.kt index ce5d8b1..6ec61dc 100644 --- a/domain/src/main/kotlin/model/Note.kt +++ b/domain/src/main/kotlin/model/Note.kt @@ -29,6 +29,7 @@ data class PersistedNote( val html: String, val updatedAt: LocalDateTime, val uuid: UUID, + val public: Boolean, ) @Serializable diff --git a/domain/src/main/kotlin/usecases/NoteService.kt b/domain/src/main/kotlin/usecases/NoteService.kt index ef672f0..7efe8ef 100644 --- a/domain/src/main/kotlin/usecases/NoteService.kt +++ b/domain/src/main/kotlin/usecases/NoteService.kt @@ -88,6 +88,10 @@ class NoteService( fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms) fun dropAllIndexes() = searcher.dropAll() + + fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid) + fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid) + fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid) } data class PaginatedNotes(val pages: Int, val notes: List) diff --git a/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt b/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt index e961c9b..e312d8f 100644 --- a/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt +++ b/domain/src/main/kotlin/usecases/repositories/NoteRepository.kt @@ -28,4 +28,8 @@ interface NoteRepository { fun update(userId: Int, uuid: UUID, note: Note): PersistedNote? fun export(userId: Int): List fun findAllDetails(userId: Int): List + + fun makePublic(userId: Int, uuid: UUID): Boolean + fun makePrivate(userId: Int, uuid: UUID): Boolean + fun findPublic(uuid: UUID): PersistedNote? } diff --git a/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt b/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt index 04632c5..2b2edc5 100644 --- a/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt +++ b/persistance/src/main/kotlin/notes/NoteRepositoryImpl.kt @@ -121,7 +121,8 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { markdown = note.markdown, html = note.html, updatedAt = now, - uuid = uuid + uuid = uuid, + public = false, // TODO ) } } @@ -199,6 +200,32 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository { } } + override fun makePublic(userId: Int, uuid: UUID) = db.update(Notes) { + it.public to true + where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) } + } == 1 + + override fun makePrivate(userId: Int, uuid: UUID) = db.update(Notes) { + it.public to false + where { Notes.userId eq userId and (Notes.uuid eq uuid) and (it.deleted eq false) } + } == 1 + + override fun findPublic(uuid: UUID): PersistedNote? { + val note = db.notes + .filterColumns { it.columns - it.userId } + .filter { it.uuid eq uuid } + .filter { it.public eq true } + .find { it.deleted eq false } + ?: return null + + val tags = db.from(Tags) + .select(Tags.name) + .where { Tags.noteUuid eq uuid } + .map { it[Tags.name]!! } + + return note.toPersistedNote(tags) + } + private fun List.tagsByUuid(): Map> { return if (isEmpty()) emptyMap() else db.tags diff --git a/persistance/src/main/kotlin/notes/Notes.kt b/persistance/src/main/kotlin/notes/Notes.kt index b7314ec..f943c8a 100644 --- a/persistance/src/main/kotlin/notes/Notes.kt +++ b/persistance/src/main/kotlin/notes/Notes.kt @@ -27,6 +27,7 @@ internal open class Notes(alias: String?) : Table("Notes", alias) { val userId = int("user_id").references(Users) { it.user } val updatedAt = datetime("updated_at").bindTo { it.updatedAt } val deleted = boolean("deleted").bindTo { it.deleted } + val public = boolean("public").bindTo { it.public } val user get() = userId.referenceTable as Users } @@ -41,6 +42,7 @@ internal interface NoteEntity : Entity { var html: String var updatedAt: LocalDateTime var deleted: Boolean + var public: Boolean var user: UserEntity } @@ -48,7 +50,7 @@ internal interface NoteEntity : Entity { internal fun NoteEntity.toPersistedMetadata(tags: List) = PersistedNoteMetadata(title, tags, updatedAt, uuid) internal fun NoteEntity.toPersistedNote(tags: List) = - PersistedNote(NoteMetadata(title, tags), markdown, html, updatedAt, uuid) + PersistedNote(NoteMetadata(title, tags), markdown, html, updatedAt, uuid, public) internal fun Note.toEntity(uuid: UUID, userId: Int): NoteEntity { val note = this @@ -58,6 +60,7 @@ internal fun Note.toEntity(uuid: UUID, userId: Int): NoteEntity { this.html = note.html this.uuid = uuid this.deleted = false + this.public = false this.user["id"] = userId } } diff --git a/persistance/src/main/resources/db/migration/mariadb/V3__Add_public_column.sql b/persistance/src/main/resources/db/migration/mariadb/V3__Add_public_column.sql new file mode 100644 index 0000000..c109304 --- /dev/null +++ b/persistance/src/main/resources/db/migration/mariadb/V3__Add_public_column.sql @@ -0,0 +1,2 @@ +alter table Notes + add column public bool not null default false diff --git a/persistance/src/main/resources/db/migration/other/V3__Add_public_column.sql b/persistance/src/main/resources/db/migration/other/V3__Add_public_column.sql new file mode 100644 index 0000000..c109304 --- /dev/null +++ b/persistance/src/main/resources/db/migration/other/V3__Add_public_column.sql @@ -0,0 +1,2 @@ +alter table Notes + add column public bool not null default false diff --git a/search/src/test/kotlin/NoteSearcherImplTest.kt b/search/src/test/kotlin/NoteSearcherImplTest.kt index dcc7e34..2fdd159 100644 --- a/search/src/test/kotlin/NoteSearcherImplTest.kt +++ b/search/src/test/kotlin/NoteSearcherImplTest.kt @@ -25,7 +25,12 @@ internal class NoteSearcherImplTest { content: String = "", uuid: UUID = UUID.randomUUID(), ): PersistedNote { - val note = PersistedNote(NoteMetadata(title, tags), markdown = content, html = "", LocalDateTime.MIN, uuid) + val note = PersistedNote(NoteMetadata(title, tags), + markdown = content, + html = "", + LocalDateTime.MIN, + uuid, + public = false) searcher.indexNote(1, note) return note }