Compare commits
3 Commits
1cb6c731d8
...
b015f3a97e
| Author | SHA1 | Date | |
|---|---|---|---|
| b015f3a97e | |||
| b8e9d4e96e | |||
| c5f9a1d6e0 |
@ -6,4 +6,5 @@ MYSQL_ROOT_PASSWORD=
|
|||||||
#
|
#
|
||||||
## can be generated with `openssl rand -base64 32`
|
## can be generated with `openssl rand -base64 32`
|
||||||
MYSQL_PASSWORD=
|
MYSQL_PASSWORD=
|
||||||
PASSWORD=${MYSQL_PASSWORD}
|
# password should be the same as mysql_password
|
||||||
|
PASSWORD=
|
||||||
|
|||||||
19
README.md
Normal file
19
README.md
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
# SimpleNotes, a simple markdown note taking website
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
- Docker
|
||||||
|
- docker-compose
|
||||||
|
|
||||||
|
## How to run
|
||||||
|
|
||||||
|
- Copy the docker-compose.yml somewhere
|
||||||
|
- In the same directory, copy the *.env.dist* file and rename it to *.env*
|
||||||
|
- Edit the variables inside *.env* (see below)
|
||||||
|
- Run it with `docker-compose up -d`
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
The app is configured with environments variables.
|
||||||
|
If no match is found within the env, a default value is read from a properties file in /app/src/main/resources/application.properties.
|
||||||
|
Don't use the default values for secrets ! Every value inside *.env.dist* should be changed.
|
||||||
@ -68,15 +68,28 @@ class NoteController(
|
|||||||
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||||
|
|
||||||
if (request.method == Method.POST && request.form("delete") != null) {
|
if (request.method == Method.POST) {
|
||||||
return if (noteService.trash(jwtPayload.userId, noteUuid))
|
if (request.form("delete") != null) {
|
||||||
Response.redirect("/notes") // TODO: flash cookie to show success ?
|
return if (noteService.trash(jwtPayload.userId, noteUuid))
|
||||||
else
|
Response.redirect("/notes") // TODO: flash cookie to show success ?
|
||||||
Response(NOT_FOUND) // TODO: show an error
|
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)
|
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 {
|
fun edit(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
|
|||||||
@ -42,6 +42,7 @@ class Router(
|
|||||||
"/login" bind GET public userController::login,
|
"/login" bind GET public userController::login,
|
||||||
"/login" bind POST public userController::login,
|
"/login" bind POST public userController::login,
|
||||||
"/logout" bind POST to userController::logout,
|
"/logout" bind POST to userController::logout,
|
||||||
|
"/notes/public/{uuid}" bind GET public noteController::public,
|
||||||
)
|
)
|
||||||
|
|
||||||
val protectedRoutes = routes(
|
val protectedRoutes = routes(
|
||||||
|
|||||||
@ -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,
|
note.meta.title,
|
||||||
jwtPayload = jwtPayload,
|
jwtPayload = jwtPayload,
|
||||||
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") {
|
||||||
|
|
||||||
|
if (shared) {
|
||||||
|
p("p-4 bg-gray-800") {
|
||||||
|
+"You are viewing a public note "
|
||||||
|
}
|
||||||
|
|
||||||
|
hr { }
|
||||||
|
}
|
||||||
|
|
||||||
div("flex items-center justify-between mb-4") {
|
div("flex items-center justify-between mb-4") {
|
||||||
h1("text-3xl fond-bold underline") { +note.meta.title }
|
h1("text-3xl fond-bold underline") { +note.meta.title }
|
||||||
span("space-x-2") {
|
span("space-x-2") {
|
||||||
@ -132,19 +141,21 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
span("flex space-x-2 justify-end mb-4") {
|
if (!shared) {
|
||||||
a(
|
noteActionForm(note)
|
||||||
href = "/notes/${note.uuid}/edit",
|
publicPrivateForm(note)
|
||||||
classes = "btn btn-teal"
|
|
||||||
) { +"Edit" }
|
if (note.public) {
|
||||||
form(method = FormMethod.post, classes = "inline") {
|
p("my-4") {
|
||||||
button(
|
+"You can share this link : "
|
||||||
type = ButtonType.submit,
|
a(href = "/notes/public/${note.uuid}", classes = "text-blue-300 underline") {
|
||||||
name = "delete",
|
+"/notes/public/${note.uuid}"
|
||||||
classes = "btn btn-red"
|
}
|
||||||
) { +"Delete" }
|
}
|
||||||
|
hr { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
div {
|
div {
|
||||||
attributes["id"] = "note"
|
attributes["id"] = "note"
|
||||||
unsafe {
|
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 ?"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -29,6 +29,7 @@ data class PersistedNote(
|
|||||||
val html: String,
|
val html: String,
|
||||||
val updatedAt: LocalDateTime,
|
val updatedAt: LocalDateTime,
|
||||||
val uuid: UUID,
|
val uuid: UUID,
|
||||||
|
val public: Boolean,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@ -88,6 +88,10 @@ class NoteService(
|
|||||||
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
|
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
|
||||||
|
|
||||||
fun dropAllIndexes() = searcher.dropAll()
|
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<PersistedNoteMetadata>)
|
data class PaginatedNotes(val pages: Int, val notes: List<PersistedNoteMetadata>)
|
||||||
|
|||||||
@ -28,4 +28,8 @@ interface NoteRepository {
|
|||||||
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
|
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
|
||||||
fun export(userId: Int): List<ExportedNote>
|
fun export(userId: Int): List<ExportedNote>
|
||||||
fun findAllDetails(userId: Int): List<PersistedNote>
|
fun findAllDetails(userId: Int): List<PersistedNote>
|
||||||
|
|
||||||
|
fun makePublic(userId: Int, uuid: UUID): Boolean
|
||||||
|
fun makePrivate(userId: Int, uuid: UUID): Boolean
|
||||||
|
fun findPublic(uuid: UUID): PersistedNote?
|
||||||
}
|
}
|
||||||
|
|||||||
@ -121,7 +121,8 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
|||||||
markdown = note.markdown,
|
markdown = note.markdown,
|
||||||
html = note.html,
|
html = note.html,
|
||||||
updatedAt = now,
|
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<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> {
|
private fun List<NoteEntity>.tagsByUuid(): Map<UUID, List<String>> {
|
||||||
return if (isEmpty()) emptyMap()
|
return if (isEmpty()) emptyMap()
|
||||||
else db.tags
|
else db.tags
|
||||||
|
|||||||
@ -27,6 +27,7 @@ internal open class Notes(alias: String?) : Table<NoteEntity>("Notes", alias) {
|
|||||||
val userId = int("user_id").references(Users) { it.user }
|
val userId = int("user_id").references(Users) { it.user }
|
||||||
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
|
val updatedAt = datetime("updated_at").bindTo { it.updatedAt }
|
||||||
val deleted = boolean("deleted").bindTo { it.deleted }
|
val deleted = boolean("deleted").bindTo { it.deleted }
|
||||||
|
val public = boolean("public").bindTo { it.public }
|
||||||
val user get() = userId.referenceTable as Users
|
val user get() = userId.referenceTable as Users
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -41,6 +42,7 @@ internal interface NoteEntity : Entity<NoteEntity> {
|
|||||||
var html: String
|
var html: String
|
||||||
var updatedAt: LocalDateTime
|
var updatedAt: LocalDateTime
|
||||||
var deleted: Boolean
|
var deleted: Boolean
|
||||||
|
var public: Boolean
|
||||||
|
|
||||||
var user: UserEntity
|
var user: UserEntity
|
||||||
}
|
}
|
||||||
@ -48,7 +50,7 @@ internal interface NoteEntity : Entity<NoteEntity> {
|
|||||||
internal fun NoteEntity.toPersistedMetadata(tags: List<String>) = PersistedNoteMetadata(title, tags, updatedAt, uuid)
|
internal fun NoteEntity.toPersistedMetadata(tags: List<String>) = PersistedNoteMetadata(title, tags, updatedAt, uuid)
|
||||||
|
|
||||||
internal fun NoteEntity.toPersistedNote(tags: List<String>) =
|
internal fun NoteEntity.toPersistedNote(tags: List<String>) =
|
||||||
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 {
|
internal fun Note.toEntity(uuid: UUID, userId: Int): NoteEntity {
|
||||||
val note = this
|
val note = this
|
||||||
@ -58,6 +60,7 @@ internal fun Note.toEntity(uuid: UUID, userId: Int): NoteEntity {
|
|||||||
this.html = note.html
|
this.html = note.html
|
||||||
this.uuid = uuid
|
this.uuid = uuid
|
||||||
this.deleted = false
|
this.deleted = false
|
||||||
|
this.public = false
|
||||||
this.user["id"] = userId
|
this.user["id"] = userId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,2 @@
|
|||||||
|
alter table Notes
|
||||||
|
add column public bool not null default false
|
||||||
@ -0,0 +1,2 @@
|
|||||||
|
alter table Notes
|
||||||
|
add column public bool not null default false
|
||||||
@ -94,13 +94,13 @@ internal class NoteRepositoryImplTest {
|
|||||||
val note = Note(NoteMetadata("title", emptyList()), "md", "html")
|
val note = Note(NoteMetadata("title", emptyList()), "md", "html")
|
||||||
|
|
||||||
assertThat(noteRepo.create(user1.id, note))
|
assertThat(noteRepo.create(user1.id, note))
|
||||||
.isEqualToIgnoringGivenFields(note, "uuid", "updatedAt")
|
.isEqualToIgnoringGivenFields(note, "uuid", "updatedAt", "public")
|
||||||
.hasNoNullFieldsOrProperties()
|
.hasNoNullFieldsOrProperties()
|
||||||
|
|
||||||
assertThat(db.notes.toList())
|
assertThat(db.notes.toList())
|
||||||
.hasSize(1)
|
.hasSize(1)
|
||||||
.first()
|
.first()
|
||||||
.isEqualToIgnoringGivenFields(note, "uuid", "updatedAt")
|
.isEqualToIgnoringGivenFields(note, "uuid", "updatedAt", "public")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -25,7 +25,12 @@ internal class NoteSearcherImplTest {
|
|||||||
content: String = "",
|
content: String = "",
|
||||||
uuid: UUID = UUID.randomUUID(),
|
uuid: UUID = UUID.randomUUID(),
|
||||||
): PersistedNote {
|
): 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)
|
searcher.indexNote(1, note)
|
||||||
return note
|
return note
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user