Add API
This commit is contained in:
parent
b015f3a97e
commit
c7cf71441f
@ -1,5 +1,7 @@
|
||||
package be.simplenotes.app
|
||||
|
||||
import be.simplenotes.app.api.ApiNoteController
|
||||
import be.simplenotes.app.api.ApiUserController
|
||||
import be.simplenotes.app.controllers.BaseController
|
||||
import be.simplenotes.app.controllers.NoteController
|
||||
import be.simplenotes.app.controllers.SettingsController
|
||||
@ -7,6 +9,7 @@ import be.simplenotes.app.controllers.UserController
|
||||
import be.simplenotes.app.filters.AuthFilter
|
||||
import be.simplenotes.app.filters.AuthType
|
||||
import be.simplenotes.app.filters.ErrorFilter
|
||||
import be.simplenotes.app.filters.JwtSource
|
||||
import be.simplenotes.app.routes.Router
|
||||
import be.simplenotes.app.utils.StaticFileResolver
|
||||
import be.simplenotes.app.utils.StaticFileResolverImpl
|
||||
@ -18,12 +21,22 @@ import be.simplenotes.persistance.persistanceModule
|
||||
import be.simplenotes.search.searchModule
|
||||
import be.simplenotes.shared.config.DataSourceConfig
|
||||
import be.simplenotes.shared.config.JwtConfig
|
||||
import kotlinx.serialization.KSerializer
|
||||
import kotlinx.serialization.descriptors.PrimitiveKind
|
||||
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
|
||||
import kotlinx.serialization.descriptors.SerialDescriptor
|
||||
import kotlinx.serialization.encoding.Decoder
|
||||
import kotlinx.serialization.encoding.Encoder
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.koin.core.context.startKoin
|
||||
import org.koin.core.qualifier.named
|
||||
import org.koin.core.qualifier.qualifier
|
||||
import org.koin.dsl.module
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import be.simplenotes.shared.config.ServerConfig as SimpleNotesServeConfig
|
||||
|
||||
fun main() {
|
||||
@ -38,6 +51,7 @@ fun main() {
|
||||
noteModule,
|
||||
settingsModule,
|
||||
searchModule,
|
||||
apiModule,
|
||||
)
|
||||
}.koin
|
||||
|
||||
@ -68,9 +82,12 @@ val serverModule = module {
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
get(),
|
||||
requiredAuth = get(AuthType.Required.qualifier),
|
||||
optionalAuth = get(AuthType.Optional.qualifier),
|
||||
errorFilter = get(named("ErrorFilter")),
|
||||
apiAuth = get(named("apiAuthFilter")),
|
||||
get()
|
||||
)()
|
||||
}
|
||||
@ -107,3 +124,57 @@ val configModule = module {
|
||||
single { Config.jwtConfig }
|
||||
single { Config.serverConfig }
|
||||
}
|
||||
|
||||
val apiModule = module {
|
||||
single { ApiUserController(get(), get()) }
|
||||
single { ApiNoteController(get(), get()) }
|
||||
single {
|
||||
Json {
|
||||
prettyPrint = true
|
||||
serializersModule = get()
|
||||
}
|
||||
}
|
||||
single {
|
||||
SerializersModule {
|
||||
contextual(LocalDateTime::class, LocalDateTimeSerializer)
|
||||
contextual(UUID::class, UuidSerializer)
|
||||
}
|
||||
}
|
||||
single(named("apiAuthFilter")) {
|
||||
AuthFilter(
|
||||
extractor = get(),
|
||||
authType = AuthType.Required,
|
||||
ctx = get(),
|
||||
source = JwtSource.Header,
|
||||
redirect = false
|
||||
)()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
internal object LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: LocalDateTime) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||
TODO("Not implemented, isn't needed")
|
||||
}
|
||||
}
|
||||
|
||||
internal object UuidSerializer : KSerializer<UUID> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UUID) {
|
||||
encoder.encodeString(value.toString())
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): UUID {
|
||||
TODO()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
77
app/src/main/kotlin/api/ApiNoteController.kt
Normal file
77
app/src/main/kotlin/api/ApiNoteController.kt
Normal file
@ -0,0 +1,77 @@
|
||||
package be.simplenotes.app.api
|
||||
|
||||
import be.simplenotes.app.extensions.json
|
||||
import be.simplenotes.app.utils.parseSearchTerms
|
||||
import be.simplenotes.domain.model.PersistedNote
|
||||
import be.simplenotes.domain.model.PersistedNoteMetadata
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.NoteService
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
import org.http4k.routing.path
|
||||
import java.util.*
|
||||
|
||||
class ApiNoteController(private val noteService: NoteService, private val json: Json) {
|
||||
|
||||
fun createNote(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
|
||||
return noteService.create(jwtPayload.userId, content).fold(
|
||||
{
|
||||
Response(BAD_REQUEST)
|
||||
},
|
||||
{
|
||||
Response(OK).json(json.encodeToString(UuidContent.serializer(), UuidContent(it.uuid)))
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
fun notes(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
|
||||
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
|
||||
return Response(OK).json(json)
|
||||
}
|
||||
|
||||
fun note(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val uuid = request.path("uuid")!!
|
||||
|
||||
return noteService.find(jwtPayload.userId, UUID.fromString(uuid))
|
||||
?.let { Response(OK).json(json.encodeToString(PersistedNote.serializer(), it)) }
|
||||
?: Response(NOT_FOUND)
|
||||
}
|
||||
|
||||
fun update(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val uuid = UUID.fromString(request.path("uuid")!!)
|
||||
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
|
||||
return noteService.update(jwtPayload.userId, uuid, content).fold({
|
||||
Response(BAD_REQUEST)
|
||||
}, {
|
||||
if (it == null) Response(NOT_FOUND)
|
||||
else Response(OK)
|
||||
})
|
||||
}
|
||||
|
||||
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val query = json.decodeFromString(SearchContent.serializer(), request.bodyString()).query
|
||||
val terms = parseSearchTerms(query)
|
||||
val notes = noteService.search(jwtPayload.userId, terms)
|
||||
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
|
||||
return Response(OK).json(json)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class NoteContent(val content: String)
|
||||
|
||||
@Serializable
|
||||
data class UuidContent(@Contextual val uuid: UUID)
|
||||
|
||||
@Serializable
|
||||
data class SearchContent(@Contextual val query: String)
|
||||
26
app/src/main/kotlin/api/ApiUserController.kt
Normal file
26
app/src/main/kotlin/api/ApiUserController.kt
Normal file
@ -0,0 +1,26 @@
|
||||
package be.simplenotes.app.api
|
||||
|
||||
import be.simplenotes.app.extensions.json
|
||||
import be.simplenotes.domain.usecases.UserService
|
||||
import be.simplenotes.domain.usecases.users.login.LoginForm
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status
|
||||
|
||||
class ApiUserController(private val userService: UserService, private val json: Json) {
|
||||
|
||||
fun login(request: Request): Response {
|
||||
val form = json.decodeFromString(LoginForm.serializer(), request.bodyString())
|
||||
val result = userService.login(form)
|
||||
return result.fold({
|
||||
Response(Status.BAD_REQUEST)
|
||||
}, {
|
||||
Response(Status.OK).json(json.encodeToString(Token.serializer(), Token(it)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class Token(val token: String)
|
||||
@ -9,6 +9,8 @@ fun Response.html(html: String) = body(html)
|
||||
.header("Content-Type", "text/html; charset=utf-8")
|
||||
.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) =
|
||||
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
|
||||
|
||||
|
||||
@ -3,10 +3,8 @@ package be.simplenotes.app.filters
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.security.JwtPayloadExtractor
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Status.Companion.UNAUTHORIZED
|
||||
import org.http4k.core.cookie.cookie
|
||||
|
||||
enum class AuthType {
|
||||
@ -18,17 +16,26 @@ private const val authKey = "auth"
|
||||
class AuthFilter(
|
||||
private val extractor: JwtPayloadExtractor,
|
||||
private val authType: AuthType,
|
||||
private val ctx: RequestContexts
|
||||
private val ctx: RequestContexts,
|
||||
private val source: JwtSource = JwtSource.Cookie,
|
||||
private val redirect: Boolean = true,
|
||||
) {
|
||||
operator fun invoke() = Filter { next ->
|
||||
{
|
||||
val jwtPayload = it.bearerToken()?.let { token -> extractor(token) }
|
||||
val token = when (source) {
|
||||
JwtSource.Header -> it.bearerTokenHeader()
|
||||
JwtSource.Cookie -> it.bearerTokenCookie()
|
||||
}
|
||||
val jwtPayload = token?.let { token -> extractor(token) }
|
||||
when {
|
||||
jwtPayload != null -> {
|
||||
ctx[it][authKey] = jwtPayload
|
||||
next(it)
|
||||
}
|
||||
authType == AuthType.Required -> Response.redirect("/login")
|
||||
authType == AuthType.Required -> {
|
||||
if (redirect) Response.redirect("/login")
|
||||
else Response(UNAUTHORIZED)
|
||||
}
|
||||
else -> next(it)
|
||||
}
|
||||
}
|
||||
@ -37,6 +44,17 @@ class AuthFilter(
|
||||
|
||||
fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey]
|
||||
|
||||
private fun Request.bearerToken(): String? = cookie("Bearer")
|
||||
enum class JwtSource {
|
||||
Header, Cookie
|
||||
}
|
||||
|
||||
private fun Request.bearerTokenCookie(): String? = cookie("Bearer")
|
||||
?.value
|
||||
?.trim()
|
||||
|
||||
private fun Request.bearerTokenHeader(): String? =
|
||||
header("Authorization")
|
||||
?.trim()
|
||||
?.takeIf { it.startsWith("Bearer") }
|
||||
?.substringAfter("Bearer")
|
||||
?.trim()
|
||||
|
||||
@ -6,14 +6,9 @@ import org.http4k.core.Method
|
||||
import org.http4k.core.Request
|
||||
|
||||
object ImmutableFilter {
|
||||
operator fun invoke(): Filter {
|
||||
return Filter { next: HttpHandler ->
|
||||
{ request: Request ->
|
||||
val response = next(request)
|
||||
if (request.method == Method.GET)
|
||||
response.header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
else response
|
||||
}
|
||||
operator fun invoke() = Filter { next: HttpHandler ->
|
||||
{ request: Request ->
|
||||
next(request).header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.app.api.ApiNoteController
|
||||
import be.simplenotes.app.api.ApiUserController
|
||||
import be.simplenotes.app.controllers.BaseController
|
||||
import be.simplenotes.app.controllers.NoteController
|
||||
import be.simplenotes.app.controllers.SettingsController
|
||||
@ -9,8 +11,7 @@ import be.simplenotes.app.filters.SecurityFilter
|
||||
import be.simplenotes.app.filters.jwtPayload
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Method.POST
|
||||
import org.http4k.core.Method.*
|
||||
import org.http4k.filter.ResponseFilters
|
||||
import org.http4k.filter.ServerFilters.InitialiseRequestContext
|
||||
import org.http4k.routing.*
|
||||
@ -20,9 +21,12 @@ class Router(
|
||||
private val userController: UserController,
|
||||
private val noteController: NoteController,
|
||||
private val settingsController: SettingsController,
|
||||
private val apiUserController: ApiUserController,
|
||||
private val apiNoteController: ApiNoteController,
|
||||
private val requiredAuth: Filter,
|
||||
private val optionalAuth: Filter,
|
||||
private val errorFilter: Filter,
|
||||
private val apiAuth: Filter,
|
||||
private val contexts: RequestContexts,
|
||||
) {
|
||||
operator fun invoke(): RoutingHttpHandler {
|
||||
@ -61,10 +65,24 @@ class Router(
|
||||
"/notes/deleted/{uuid}" bind POST protected noteController::deleted,
|
||||
)
|
||||
|
||||
val apiRoutes = routes(
|
||||
"/api/login" bind POST to apiUserController::login,
|
||||
)
|
||||
|
||||
val protectedApiRoutes = routes(
|
||||
"/api/notes" bind GET protected apiNoteController::notes,
|
||||
"/api/notes" bind POST protected apiNoteController::createNote,
|
||||
"/api/notes/search" bind POST protected apiNoteController::search,
|
||||
"/api/notes/{uuid}" bind GET protected apiNoteController::note,
|
||||
"/api/notes/{uuid}" bind PUT protected apiNoteController::update,
|
||||
)
|
||||
|
||||
val routes = routes(
|
||||
basicRoutes,
|
||||
optionalAuth.then(publicRoutes),
|
||||
requiredAuth.then(protectedRoutes),
|
||||
apiAuth.then(protectedApiRoutes),
|
||||
apiRoutes,
|
||||
)
|
||||
|
||||
val globalFilters = errorFilter
|
||||
|
||||
@ -5,30 +5,34 @@ import kotlinx.serialization.Serializable
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
|
||||
@Serializable
|
||||
data class NoteMetadata(
|
||||
val title: String,
|
||||
val tags: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PersistedNoteMetadata(
|
||||
val title: String,
|
||||
val tags: List<String>,
|
||||
val updatedAt: LocalDateTime,
|
||||
val uuid: UUID,
|
||||
@Contextual val updatedAt: LocalDateTime,
|
||||
@Contextual val uuid: UUID,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Note(
|
||||
val meta: NoteMetadata,
|
||||
val markdown: String,
|
||||
val html: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PersistedNote(
|
||||
val meta: NoteMetadata,
|
||||
val markdown: String,
|
||||
val html: String,
|
||||
val updatedAt: LocalDateTime,
|
||||
val uuid: UUID,
|
||||
@Contextual val updatedAt: LocalDateTime,
|
||||
@Contextual val uuid: UUID,
|
||||
val public: Boolean,
|
||||
)
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import be.simplenotes.domain.usecases.repositories.NoteRepository
|
||||
import be.simplenotes.domain.usecases.repositories.UserRepository
|
||||
import be.simplenotes.domain.usecases.search.NoteSearcher
|
||||
import be.simplenotes.domain.usecases.search.SearchTerms
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.*
|
||||
|
||||
class NoteService(
|
||||
|
||||
@ -2,6 +2,7 @@ package be.simplenotes.domain.usecases.users.login
|
||||
|
||||
import arrow.core.Either
|
||||
import io.konform.validation.ValidationErrors
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
sealed class LoginError
|
||||
object Unregistered : LoginError()
|
||||
@ -10,6 +11,7 @@ class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError()
|
||||
|
||||
typealias Token = String
|
||||
|
||||
@Serializable
|
||||
data class LoginForm(val username: String?, val password: String?)
|
||||
|
||||
interface LoginUseCase {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user