Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ff97044f0 | |||
| ead1932d48 | |||
| 4a7dcec363 | |||
| cb76a3253d |
@@ -1,6 +1,6 @@
|
|||||||
package be.simplenotes.app.api
|
package be.simplenotes.app.api
|
||||||
|
|
||||||
import be.simplenotes.app.extensions.json
|
import be.simplenotes.app.extensions.auto
|
||||||
import be.simplenotes.app.utils.parseSearchTerms
|
import be.simplenotes.app.utils.parseSearchTerms
|
||||||
import be.simplenotes.domain.model.PersistedNote
|
import be.simplenotes.domain.model.PersistedNote
|
||||||
import be.simplenotes.domain.model.PersistedNoteMetadata
|
import be.simplenotes.domain.model.PersistedNoteMetadata
|
||||||
@@ -8,48 +8,39 @@ import be.simplenotes.domain.security.JwtPayload
|
|||||||
import be.simplenotes.domain.usecases.NoteService
|
import be.simplenotes.domain.usecases.NoteService
|
||||||
import kotlinx.serialization.Contextual
|
import kotlinx.serialization.Contextual
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.builtins.ListSerializer
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
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.BAD_REQUEST
|
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||||
import org.http4k.core.Status.Companion.NOT_FOUND
|
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||||
import org.http4k.core.Status.Companion.OK
|
import org.http4k.core.Status.Companion.OK
|
||||||
import org.http4k.routing.path
|
import org.http4k.lens.Path
|
||||||
|
import org.http4k.lens.uuid
|
||||||
import java.util.*
|
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, jwtPayload: JwtPayload): Response {
|
||||||
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
|
val content = noteContentLens(request)
|
||||||
return noteService.create(jwtPayload.userId, content).fold(
|
return noteService.create(jwtPayload.userId, content).fold(
|
||||||
{
|
{ Response(BAD_REQUEST) },
|
||||||
Response(BAD_REQUEST)
|
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
|
||||||
},
|
|
||||||
{
|
|
||||||
Response(OK).json(json.encodeToString(UuidContent.serializer(), UuidContent(it.uuid)))
|
|
||||||
}
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun notes(request: Request, jwtPayload: JwtPayload): Response {
|
fun notes(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
|
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
|
||||||
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
|
return persistedNotesMetadataLens(notes, Response(OK))
|
||||||
return Response(OK).json(json)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
fun note(request: Request, jwtPayload: JwtPayload): Response =
|
||||||
val uuid = request.path("uuid")!!
|
noteService.find(jwtPayload.userId, uuidLens(request))
|
||||||
|
?.let { persistedNoteLens(it, Response(OK)) }
|
||||||
return noteService.find(jwtPayload.userId, UUID.fromString(uuid))
|
|
||||||
?.let { Response(OK).json(json.encodeToString(PersistedNote.serializer(), it)) }
|
|
||||||
?: Response(NOT_FOUND)
|
?: Response(NOT_FOUND)
|
||||||
}
|
|
||||||
|
|
||||||
fun update(request: Request, jwtPayload: JwtPayload): Response {
|
fun update(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
val uuid = UUID.fromString(request.path("uuid")!!)
|
val content = noteContentLens(request)
|
||||||
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
|
return noteService.update(jwtPayload.userId, uuidLens(request), content).fold({
|
||||||
return noteService.update(jwtPayload.userId, uuid, content).fold({
|
|
||||||
Response(BAD_REQUEST)
|
Response(BAD_REQUEST)
|
||||||
}, {
|
}, {
|
||||||
if (it == null) Response(NOT_FOUND)
|
if (it == null) Response(NOT_FOUND)
|
||||||
@@ -58,13 +49,19 @@ class ApiNoteController(private val noteService: NoteService, private val json:
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
val query = json.decodeFromString(SearchContent.serializer(), request.bodyString()).query
|
val query = searchContentLens(request)
|
||||||
val terms = parseSearchTerms(query)
|
val terms = parseSearchTerms(query)
|
||||||
val notes = noteService.search(jwtPayload.userId, terms)
|
val notes = noteService.search(jwtPayload.userId, terms)
|
||||||
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
|
return persistedNotesMetadataLens(notes, Response(OK))
|
||||||
return Response(OK).json(json)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private val uuidContentLens = json.auto<UuidContent>().toLens()
|
||||||
|
private val noteContentLens = json.auto<NoteContent>().map { it.content }.toLens()
|
||||||
|
private val searchContentLens = json.auto<SearchContent>().map { it.query }.toLens()
|
||||||
|
private val persistedNotesMetadataLens = json.auto<List<PersistedNoteMetadata>>().toLens()
|
||||||
|
private val persistedNoteLens = json.auto<PersistedNote>().toLens()
|
||||||
|
private val uuidLens = Path.uuid().of("uuid")
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -1,25 +1,25 @@
|
|||||||
package be.simplenotes.app.api
|
package be.simplenotes.app.api
|
||||||
|
|
||||||
import be.simplenotes.app.extensions.json
|
import be.simplenotes.app.extensions.auto
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.usecases.UserService
|
||||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
import be.simplenotes.domain.usecases.users.login.LoginForm
|
||||||
import kotlinx.serialization.Serializable
|
import kotlinx.serialization.Serializable
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
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
|
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||||
|
import org.http4k.core.Status.Companion.OK
|
||||||
|
|
||||||
class ApiUserController(private val userService: UserService, private val json: Json) {
|
class ApiUserController(private val userService: UserService, private val json: Json) {
|
||||||
|
private val tokenLens = json.auto<Token>().toLens()
|
||||||
|
private val loginFormLens = json.auto<LoginForm>().toLens()
|
||||||
|
|
||||||
fun login(request: Request): Response {
|
fun login(request: Request) = userService
|
||||||
val form = json.decodeFromString(LoginForm.serializer(), request.bodyString())
|
.login(loginFormLens(request))
|
||||||
val result = userService.login(form)
|
.fold(
|
||||||
return result.fold({
|
{ Response(BAD_REQUEST) },
|
||||||
Response(Status.BAD_REQUEST)
|
{ tokenLens(Token(it), Response(OK)) }
|
||||||
}, {
|
)
|
||||||
Response(Status.OK).json(json.encodeToString(Token.serializer(), Token(it)))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -1,17 +1,35 @@
|
|||||||
package be.simplenotes.app.extensions
|
package be.simplenotes.app.extensions
|
||||||
|
|
||||||
|
import kotlinx.serialization.decodeFromString
|
||||||
|
import kotlinx.serialization.encodeToString
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import org.http4k.asString
|
||||||
|
import org.http4k.core.Body
|
||||||
|
import org.http4k.core.ContentType
|
||||||
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.FOUND
|
import org.http4k.core.Status.Companion.FOUND
|
||||||
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
|
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
|
||||||
|
import org.http4k.lens.*
|
||||||
|
|
||||||
fun Response.html(html: String) = body(html)
|
fun Response.html(html: String) = body(html)
|
||||||
.header("Content-Type", "text/html; charset=utf-8")
|
.header("Content-Type", "text/html; charset=utf-8")
|
||||||
.header("Cache-Control", "no-cache")
|
.header("Cache-Control", "no-cache")
|
||||||
|
|
||||||
fun Response.json(json: String) = body(json).header("Content-Type", "application/json")
|
|
||||||
|
|
||||||
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
|
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
|
||||||
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
|
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
|
||||||
|
|
||||||
fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
|
fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
|
||||||
|
|
||||||
|
val bodyLens = httpBodyRoot(
|
||||||
|
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
|
||||||
|
ContentType.APPLICATION_JSON.withNoDirectives(), ContentNegotiation.StrictNoDirective
|
||||||
|
).map(
|
||||||
|
{ it.payload.asString() },
|
||||||
|
{ Body(it) }
|
||||||
|
)
|
||||||
|
|
||||||
|
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
|
||||||
|
{ decodeFromString(it) },
|
||||||
|
{ encodeToString(it) }
|
||||||
|
)
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
) {
|
) {
|
||||||
section("text-center my-2 p-2") {
|
section("text-center my-2 p-2") {
|
||||||
h1("text-5xl casual") {
|
h1("text-5xl casual") {
|
||||||
span("text-teal-300") { +"Simplenotes " }
|
span("text-teal-300") { +"SimpleNotes " }
|
||||||
+"- access your notes anywhere"
|
+"- access your notes anywhere"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,7 +143,6 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
}
|
}
|
||||||
if (!shared) {
|
if (!shared) {
|
||||||
noteActionForm(note)
|
noteActionForm(note)
|
||||||
publicPrivateForm(note)
|
|
||||||
|
|
||||||
if (note.public) {
|
if (note.public) {
|
||||||
p("my-4") {
|
p("my-4") {
|
||||||
@@ -166,12 +165,29 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun DIV.noteActionForm(note: PersistedNote) {
|
private fun DIV.noteActionForm(note: PersistedNote) {
|
||||||
span("flex space-x-2 justify-end mb-4") {
|
form(method = FormMethod.post, classes = "inline flex space-x-2 justify-end mb-4") {
|
||||||
a(
|
a(
|
||||||
href = "/notes/${note.uuid}/edit",
|
href = "/notes/${note.uuid}/edit",
|
||||||
classes = "btn btn-teal"
|
classes = "btn btn-green"
|
||||||
) { +"Edit" }
|
) { +"Edit" }
|
||||||
form(method = FormMethod.post, classes = "inline") {
|
span {
|
||||||
|
button(
|
||||||
|
type = ButtonType.submit,
|
||||||
|
name = if (note.public) "private" else "public",
|
||||||
|
classes = "font-semibold border-b-4 ${if (note.public) "border-teal-200" else "border-green-500"}" +
|
||||||
|
" p-2 rounded-l bg-teal-200 text-gray-800"
|
||||||
|
) {
|
||||||
|
+"Private"
|
||||||
|
}
|
||||||
|
button(
|
||||||
|
type = ButtonType.submit,
|
||||||
|
name = if (note.public) "private" else "public",
|
||||||
|
classes = "font-semibold border-b-4 ${if (!note.public) "border-teal-200" else "border-green-500"}" +
|
||||||
|
" p-2 rounded-r bg-teal-200 text-gray-800"
|
||||||
|
) {
|
||||||
|
+"Public"
|
||||||
|
}
|
||||||
|
}
|
||||||
button(
|
button(
|
||||||
type = ButtonType.submit,
|
type = ButtonType.submit,
|
||||||
name = "delete",
|
name = "delete",
|
||||||
@@ -179,23 +195,4 @@ class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
) { +"Delete" }
|
) { +"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 ?"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,22 +26,23 @@ class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResol
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
section("m-4 p-4 bg-gray-800 rounded flex justify-around") {
|
section("m-4 p-2 bg-gray-800 rounded flex flex-wrap justify-around items-end") {
|
||||||
|
|
||||||
form(method = FormMethod.post, action = "/export") {
|
form(classes = "m-2", method = FormMethod.post, action = "/export") {
|
||||||
button(name = "display",
|
button(name = "display",
|
||||||
classes = "inline btn btn-teal block",
|
classes = "inline btn btn-teal block",
|
||||||
type = submit) { +"Display my data" }
|
type = submit) { +"Display my data" }
|
||||||
}
|
}
|
||||||
|
|
||||||
form(method = FormMethod.post, action = "/export") {
|
form(classes = "m-2", method = FormMethod.post, action = "/export") {
|
||||||
|
|
||||||
listOf("json", "zip").forEach { format ->
|
|
||||||
div {
|
div {
|
||||||
|
listOf("json", "zip").forEach { format ->
|
||||||
radioInput(name = "format") {
|
radioInput(name = "format") {
|
||||||
id = format
|
id = format
|
||||||
attributes["value"] = format
|
attributes["value"] = format
|
||||||
if(format == "json") attributes["checked"] = ""
|
if (format == "json") attributes["checked"] = ""
|
||||||
|
else attributes["class"] = "ml-4"
|
||||||
}
|
}
|
||||||
label(classes = "ml-2") {
|
label(classes = "ml-2") {
|
||||||
attributes["for"] = format
|
attributes["for"] = format
|
||||||
|
|||||||
@@ -25,8 +25,8 @@ fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) = div("over
|
|||||||
td("text-center") { +updatedAt.toTimeAgo() }
|
td("text-center") { +updatedAt.toTimeAgo() }
|
||||||
td { tags(tags) }
|
td { tags(tags) }
|
||||||
td("text-center") {
|
td("text-center") {
|
||||||
form(classes = "inline", method = post, action = "/notes/deleted/$uuid") {
|
form(method = post, action = "/notes/deleted/$uuid") {
|
||||||
button(classes = "btn btn-red", type = submit, name = "delete") {
|
button(classes = "btn btn-red mb-2", type = submit, name = "delete") {
|
||||||
+"Delete permanently"
|
+"Delete permanently"
|
||||||
}
|
}
|
||||||
button(classes = "ml-2 btn btn-green", type = submit, name = "restore") {
|
button(classes = "ml-2 btn btn-green", type = submit, name = "restore") {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import me.liuwj.ktorm.database.Database
|
|||||||
import org.koin.dsl.bind
|
import org.koin.dsl.bind
|
||||||
import org.koin.dsl.module
|
import org.koin.dsl.module
|
||||||
import org.koin.dsl.onClose
|
import org.koin.dsl.onClose
|
||||||
import org.mapstruct.factory.Mappers
|
|
||||||
import javax.sql.DataSource
|
import javax.sql.DataSource
|
||||||
|
|
||||||
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
|
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
|
||||||
|
|||||||
@@ -8,9 +8,10 @@ import org.mapstruct.Mapper
|
|||||||
|
|
||||||
@Mapper(uses = [UserEntityFactory::class])
|
@Mapper(uses = [UserEntityFactory::class])
|
||||||
internal interface UserConverter {
|
internal interface UserConverter {
|
||||||
fun convertToUser(userEntity: UserEntity): User
|
fun toUser(userEntity: UserEntity): User
|
||||||
fun convertToPersistedUser(userEntity: UserEntity): PersistedUser
|
fun toPersistedUser(userEntity: UserEntity): PersistedUser
|
||||||
fun convertToEntity(user: User): UserEntity
|
fun toEntity(user: User): UserEntity
|
||||||
|
fun toEntity(user: PersistedUser): UserEntity
|
||||||
}
|
}
|
||||||
|
|
||||||
internal class UserEntityFactory : Entity.Factory<UserEntity>()
|
internal class UserEntityFactory : Entity.Factory<UserEntity>()
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ internal class UserRepositoryImpl(private val db: Database, private val converte
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun find(username: String) = db.users.find { it.username eq username }
|
override fun find(username: String) = db.users.find { it.username eq username }
|
||||||
?.let { converter.convertToPersistedUser(it) }
|
?.let { converter.toPersistedUser(it) }
|
||||||
|
|
||||||
override fun find(id: Int) = db.users.find { it.id eq id }?.let {
|
override fun find(id: Int) = db.users.find { it.id eq id }?.let {
|
||||||
converter.convertToPersistedUser(it)
|
converter.toPersistedUser(it)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun exists(username: String) = db.users.any { it.username eq username }
|
override fun exists(username: String) = db.users.any { it.username eq username }
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ internal class UserConverterTest {
|
|||||||
}.apply {
|
}.apply {
|
||||||
this["id"] = 2
|
this["id"] = 2
|
||||||
}
|
}
|
||||||
val user = converter.convertToUser(entity)
|
val user = converter.toUser(entity)
|
||||||
assertThat(user).isEqualTo(User("test", "test2"))
|
assertThat(user).isEqualTo(User("test", "test2"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ internal class UserConverterTest {
|
|||||||
}.apply {
|
}.apply {
|
||||||
this["id"] = 2
|
this["id"] = 2
|
||||||
}
|
}
|
||||||
val user = converter.convertToPersistedUser(entity)
|
val user = converter.toPersistedUser(entity)
|
||||||
assertThat(user).isEqualTo(PersistedUser("test", "test2", 2))
|
assertThat(user).isEqualTo(PersistedUser("test", "test2", 2))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +39,7 @@ internal class UserConverterTest {
|
|||||||
fun `convert User to UserEntity`() {
|
fun `convert User to UserEntity`() {
|
||||||
val converter = Mappers.getMapper(UserConverter::class.java)
|
val converter = Mappers.getMapper(UserConverter::class.java)
|
||||||
val user = User("test", "test2")
|
val user = User("test", "test2")
|
||||||
val entity = converter.convertToEntity(user)
|
val entity = converter.toEntity(user)
|
||||||
|
|
||||||
assertThat(entity)
|
assertThat(entity)
|
||||||
.hasFieldOrPropertyWithValue("username", "test")
|
.hasFieldOrPropertyWithValue("username", "test")
|
||||||
|
|||||||
Reference in New Issue
Block a user