Merge branch 'export'
This commit is contained in:
parent
8ccd7f6058
commit
88b6eb56ae
@ -42,7 +42,6 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jetbrains.kotlinx</groupId>
|
<groupId>org.jetbrains.kotlinx</groupId>
|
||||||
<artifactId>kotlinx-serialization-runtime</artifactId>
|
<artifactId>kotlinx-serialization-runtime</artifactId>
|
||||||
<version>0.20.0</version>
|
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package be.simplenotes.app
|
|||||||
|
|
||||||
import be.simplenotes.app.controllers.BaseController
|
import be.simplenotes.app.controllers.BaseController
|
||||||
import be.simplenotes.app.controllers.NoteController
|
import be.simplenotes.app.controllers.NoteController
|
||||||
|
import be.simplenotes.app.controllers.SettingsController
|
||||||
import be.simplenotes.app.controllers.UserController
|
import be.simplenotes.app.controllers.UserController
|
||||||
import be.simplenotes.app.filters.AuthFilter
|
import be.simplenotes.app.filters.AuthFilter
|
||||||
import be.simplenotes.app.filters.AuthType
|
import be.simplenotes.app.filters.AuthType
|
||||||
@ -10,6 +11,7 @@ import be.simplenotes.app.utils.StaticFileResolver
|
|||||||
import be.simplenotes.app.utils.StaticFileResolverImpl
|
import be.simplenotes.app.utils.StaticFileResolverImpl
|
||||||
import be.simplenotes.app.views.BaseView
|
import be.simplenotes.app.views.BaseView
|
||||||
import be.simplenotes.app.views.NoteView
|
import be.simplenotes.app.views.NoteView
|
||||||
|
import be.simplenotes.app.views.SettingView
|
||||||
import be.simplenotes.app.views.UserView
|
import be.simplenotes.app.views.UserView
|
||||||
import be.simplenotes.domain.domainModule
|
import be.simplenotes.domain.domainModule
|
||||||
import be.simplenotes.persistance.DbMigrations
|
import be.simplenotes.persistance.DbMigrations
|
||||||
@ -33,6 +35,7 @@ fun main() {
|
|||||||
userModule,
|
userModule,
|
||||||
baseModule,
|
baseModule,
|
||||||
noteModule,
|
noteModule,
|
||||||
|
settingsModule,
|
||||||
)
|
)
|
||||||
}.koin
|
}.koin
|
||||||
|
|
||||||
@ -58,6 +61,7 @@ val serverModule = module {
|
|||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
get(),
|
get(),
|
||||||
|
get(),
|
||||||
requiredAuth = get(AuthType.Required.qualifier),
|
requiredAuth = get(AuthType.Required.qualifier),
|
||||||
optionalAuth = get(AuthType.Optional.qualifier),
|
optionalAuth = get(AuthType.Optional.qualifier),
|
||||||
get()
|
get()
|
||||||
@ -84,6 +88,11 @@ val noteModule = module {
|
|||||||
single { NoteView(get()) }
|
single { NoteView(get()) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val settingsModule = module {
|
||||||
|
single { SettingsController(get(), get()) }
|
||||||
|
single { SettingView(get()) }
|
||||||
|
}
|
||||||
|
|
||||||
val configModule = module {
|
val configModule = module {
|
||||||
single { Config.dataSourceConfig }
|
single { Config.dataSourceConfig }
|
||||||
single { Config.jwtConfig }
|
single { Config.jwtConfig }
|
||||||
|
|||||||
59
app/src/main/kotlin/controllers/SettingsController.kt
Normal file
59
app/src/main/kotlin/controllers/SettingsController.kt
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package be.simplenotes.app.controllers
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.html
|
||||||
|
import be.simplenotes.app.extensions.redirect
|
||||||
|
import be.simplenotes.app.views.SettingView
|
||||||
|
import be.simplenotes.domain.security.JwtPayload
|
||||||
|
import be.simplenotes.domain.usecases.UserService
|
||||||
|
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
||||||
|
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
||||||
|
import org.http4k.core.Method
|
||||||
|
import org.http4k.core.Request
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status
|
||||||
|
import org.http4k.core.body.form
|
||||||
|
import org.http4k.core.cookie.invalidateCookie
|
||||||
|
|
||||||
|
class SettingsController(
|
||||||
|
private val userService: UserService,
|
||||||
|
private val settingView: SettingView,
|
||||||
|
) {
|
||||||
|
fun settings(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
|
if (request.method == Method.GET)
|
||||||
|
return Response(Status.OK).html(settingView.settings(jwtPayload))
|
||||||
|
|
||||||
|
val deleteForm = request.deleteForm(jwtPayload)
|
||||||
|
val result = userService.delete(deleteForm)
|
||||||
|
|
||||||
|
return result.fold(
|
||||||
|
{
|
||||||
|
when (it) {
|
||||||
|
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Authorization")
|
||||||
|
DeleteError.WrongPassword -> Response(Status.OK).html(
|
||||||
|
settingView.settings(
|
||||||
|
jwtPayload,
|
||||||
|
error = "Wrong password"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
is DeleteError.InvalidForm -> Response(Status.OK).html(
|
||||||
|
settingView.settings(
|
||||||
|
jwtPayload,
|
||||||
|
validationErrors = it.validationErrors
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Response.redirect("/").invalidateCookie("Authorization")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun export(request: Request, jwtPayload: JwtPayload): Response {
|
||||||
|
val json = userService.export(jwtPayload.userId)
|
||||||
|
return Response(Status.OK).body(json).header("Content-Type", "application/json")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Request.deleteForm(jwtPayload: JwtPayload) =
|
||||||
|
DeleteForm(jwtPayload.username, form("password"), form("checked") != null)
|
||||||
|
}
|
||||||
@ -6,8 +6,6 @@ import be.simplenotes.app.extensions.redirect
|
|||||||
import be.simplenotes.app.views.UserView
|
import be.simplenotes.app.views.UserView
|
||||||
import be.simplenotes.domain.security.JwtPayload
|
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.DeleteForm
|
|
||||||
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
|
||||||
@ -112,38 +110,4 @@ class UserController(
|
|||||||
|
|
||||||
fun logout(@Suppress("UNUSED_PARAMETER") request: Request) = Response.redirect("/")
|
fun logout(@Suppress("UNUSED_PARAMETER") request: Request) = Response.redirect("/")
|
||||||
.invalidateCookie("Authorization")
|
.invalidateCookie("Authorization")
|
||||||
|
|
||||||
private fun Request.deleteForm(jwtPayload: JwtPayload) =
|
|
||||||
DeleteForm(jwtPayload.username, form("password"), form("checked") != null)
|
|
||||||
|
|
||||||
fun settings(request: Request, jwtPayload: JwtPayload): Response {
|
|
||||||
if (request.method == GET)
|
|
||||||
return Response(OK).html(userView.settings(jwtPayload))
|
|
||||||
|
|
||||||
val deleteForm = request.deleteForm(jwtPayload)
|
|
||||||
val result = userService.delete(deleteForm)
|
|
||||||
|
|
||||||
return result.fold(
|
|
||||||
{
|
|
||||||
when (it) {
|
|
||||||
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Authorization")
|
|
||||||
DeleteError.WrongPassword -> Response(OK).html(
|
|
||||||
userView.settings(
|
|
||||||
jwtPayload,
|
|
||||||
error = "Wrong password"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
is DeleteError.InvalidForm -> Response(OK).html(
|
|
||||||
userView.settings(
|
|
||||||
jwtPayload,
|
|
||||||
validationErrors = it.validationErrors
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Response.redirect("/").invalidateCookie("Authorization")
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
15
app/src/main/kotlin/extensions/KotlinxHtmlExtensions.kt
Normal file
15
app/src/main/kotlin/extensions/KotlinxHtmlExtensions.kt
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
package be.simplenotes.app.extensions
|
||||||
|
|
||||||
|
import kotlinx.html.*
|
||||||
|
|
||||||
|
class SUMMARY(consumer: TagConsumer<*>) :
|
||||||
|
HTMLTag(
|
||||||
|
"summary", consumer, emptyMap(),
|
||||||
|
inlineTag = true,
|
||||||
|
emptyTag = false
|
||||||
|
),
|
||||||
|
HtmlInlineTag
|
||||||
|
|
||||||
|
fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) {
|
||||||
|
SUMMARY(consumer).visit(block)
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ package be.simplenotes.app.routes
|
|||||||
|
|
||||||
import be.simplenotes.app.controllers.BaseController
|
import be.simplenotes.app.controllers.BaseController
|
||||||
import be.simplenotes.app.controllers.NoteController
|
import be.simplenotes.app.controllers.NoteController
|
||||||
|
import be.simplenotes.app.controllers.SettingsController
|
||||||
import be.simplenotes.app.controllers.UserController
|
import be.simplenotes.app.controllers.UserController
|
||||||
import be.simplenotes.app.filters.ErrorFilter
|
import be.simplenotes.app.filters.ErrorFilter
|
||||||
import be.simplenotes.app.filters.ImmutableFilter
|
import be.simplenotes.app.filters.ImmutableFilter
|
||||||
@ -19,6 +20,7 @@ class Router(
|
|||||||
private val baseController: BaseController,
|
private val baseController: BaseController,
|
||||||
private val userController: UserController,
|
private val userController: UserController,
|
||||||
private val noteController: NoteController,
|
private val noteController: NoteController,
|
||||||
|
private val settingsController: SettingsController,
|
||||||
private val requiredAuth: Filter,
|
private val requiredAuth: Filter,
|
||||||
private val optionalAuth: Filter,
|
private val optionalAuth: Filter,
|
||||||
private val contexts: RequestContexts,
|
private val contexts: RequestContexts,
|
||||||
@ -42,9 +44,9 @@ class Router(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val protectedRoutes = routes(
|
val protectedRoutes = routes(
|
||||||
"/settings" bind GET to { protected(it, userController::settings) },
|
"/settings" bind GET to { protected(it, settingsController::settings) },
|
||||||
"/settings" bind POST to { protected(it, userController::settings) },
|
"/settings" bind POST to { protected(it, settingsController::settings) },
|
||||||
"/export" bind POST to { TODO() },
|
"/export" bind POST to { protected(it, settingsController::export) },
|
||||||
"/notes" bind GET to { protected(it, noteController::list) },
|
"/notes" bind GET to { protected(it, noteController::list) },
|
||||||
"/notes/new" bind GET to { protected(it, noteController::new) },
|
"/notes/new" bind GET to { protected(it, noteController::new) },
|
||||||
"/notes/new" bind POST to { protected(it, noteController::new) },
|
"/notes/new" bind POST to { protected(it, noteController::new) },
|
||||||
|
|||||||
@ -1,7 +1,6 @@
|
|||||||
package be.simplenotes.app.utils
|
package be.simplenotes.app.utils
|
||||||
|
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.*
|
||||||
import kotlinx.serialization.json.JsonConfiguration
|
|
||||||
|
|
||||||
interface StaticFileResolver {
|
interface StaticFileResolver {
|
||||||
fun resolve(name: String): String?
|
fun resolve(name: String): String?
|
||||||
@ -11,12 +10,12 @@ class StaticFileResolverImpl : StaticFileResolver {
|
|||||||
private val mappings: Map<String, String>
|
private val mappings: Map<String, String>
|
||||||
|
|
||||||
init {
|
init {
|
||||||
val json = Json(JsonConfiguration.Stable)
|
val json = Json {}
|
||||||
val manifest = javaClass.getResource("/css-manifest.json").readText()
|
val manifest = javaClass.getResource("/css-manifest.json").readText()
|
||||||
val manifestObject = json.parseJson(manifest).jsonObject
|
val manifestObject = json.parseToJsonElement(manifest).jsonObject
|
||||||
val keys = manifestObject.keys
|
val keys = manifestObject.keys
|
||||||
mappings = keys.map {
|
mappings = keys.map {
|
||||||
it to "/${manifestObject[it]!!.primitive.content}"
|
it to "/${manifestObject[it]!!.jsonPrimitive.content}"
|
||||||
}.toMap()
|
}.toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
81
app/src/main/kotlin/views/SettingView.kt
Normal file
81
app/src/main/kotlin/views/SettingView.kt
Normal file
@ -0,0 +1,81 @@
|
|||||||
|
package be.simplenotes.app.views
|
||||||
|
|
||||||
|
import be.simplenotes.app.extensions.summary
|
||||||
|
import be.simplenotes.app.utils.StaticFileResolver
|
||||||
|
import be.simplenotes.app.views.components.Alert
|
||||||
|
import be.simplenotes.app.views.components.alert
|
||||||
|
import be.simplenotes.app.views.components.input
|
||||||
|
import be.simplenotes.domain.security.JwtPayload
|
||||||
|
import io.konform.validation.ValidationError
|
||||||
|
import kotlinx.html.*
|
||||||
|
import kotlinx.html.ButtonType.submit
|
||||||
|
|
||||||
|
class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
|
||||||
|
|
||||||
|
fun settings(
|
||||||
|
jwtPayload: JwtPayload,
|
||||||
|
error: String? = null,
|
||||||
|
validationErrors: List<ValidationError> = emptyList(),
|
||||||
|
) = renderPage("Settings", jwtPayload = jwtPayload) {
|
||||||
|
div("container mx-auto") {
|
||||||
|
|
||||||
|
section("m-4 p-4 bg-gray-800 rounded") {
|
||||||
|
h1("text-xl") {
|
||||||
|
+"Welcome "
|
||||||
|
span("text-teal-200 font-semibold") { +jwtPayload.username }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section("m-4 p-4 bg-gray-800 rounded") {
|
||||||
|
p(classes = "mb-4") {
|
||||||
|
+"Export all my data"
|
||||||
|
}
|
||||||
|
|
||||||
|
form(method = FormMethod.post, action = "/export") {
|
||||||
|
button(classes = "btn btn-teal block", type = submit) { +"Export my data" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
section(classes = "m-4 p-4 bg-gray-800 rounded") {
|
||||||
|
h2(classes = "mb-4 text-red-600 text-lg font-semibold") {
|
||||||
|
+"Delete my account"
|
||||||
|
}
|
||||||
|
|
||||||
|
error?.let { alert(Alert.Warning, error) }
|
||||||
|
|
||||||
|
details {
|
||||||
|
|
||||||
|
if (error != null || validationErrors.isNotEmpty()) {
|
||||||
|
attributes["open"] = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
span(classes = "mb-4 font-semibold underline") {
|
||||||
|
+"Are you sure? "
|
||||||
|
+"You are about to delete this user, and this process is irreversible !"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form(classes = "mt-4", method = FormMethod.post) {
|
||||||
|
input(
|
||||||
|
id = "password",
|
||||||
|
placeholder = "Password",
|
||||||
|
autoComplete = "off",
|
||||||
|
type = InputType.password,
|
||||||
|
error = validationErrors.find { it.dataPath == ".password" }?.message
|
||||||
|
)
|
||||||
|
checkBoxInput(name = "checked") {
|
||||||
|
attributes["required"] = ""
|
||||||
|
+" Do you want to proceed ?"
|
||||||
|
}
|
||||||
|
button(
|
||||||
|
type = submit,
|
||||||
|
classes = "block mt-4 btn btn-red",
|
||||||
|
name = "delete"
|
||||||
|
) { +"I'm sure" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -71,79 +71,4 @@ class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun settings(
|
|
||||||
jwtPayload: JwtPayload,
|
|
||||||
error: String? = null,
|
|
||||||
validationErrors: List<ValidationError> = emptyList(),
|
|
||||||
) = renderPage("Settings", jwtPayload = jwtPayload) {
|
|
||||||
div("container mx-auto") {
|
|
||||||
|
|
||||||
section("m-4 p-4 bg-gray-800 rounded") {
|
|
||||||
h1("text-xl") {
|
|
||||||
+"Welcome "
|
|
||||||
span("text-teal-200 font-semibold") { +jwtPayload.username }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section("m-4 p-4 bg-gray-800 rounded") {
|
|
||||||
p(classes = "mb-4") {
|
|
||||||
+"Export all my data in a zip file"
|
|
||||||
}
|
|
||||||
|
|
||||||
form {
|
|
||||||
button(classes = "btn btn-teal block") { +"Export my data" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
section(classes = "m-4 p-4 bg-gray-800 rounded") {
|
|
||||||
h2(classes = "mb-4 text-red-600 text-lg font-semibold") {
|
|
||||||
+"Delete my account"
|
|
||||||
}
|
|
||||||
|
|
||||||
error?.let { alert(Alert.Warning, error) }
|
|
||||||
|
|
||||||
details {
|
|
||||||
|
|
||||||
if (error != null || validationErrors.isNotEmpty()) {
|
|
||||||
attributes["open"] = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
summary {
|
|
||||||
span(classes = "mb-4 font-semibold underline") {
|
|
||||||
+"Are you sure? "
|
|
||||||
+"You are about to delete this user, and this process is irreversible !"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
form(classes = "mt-4", method = FormMethod.post) {
|
|
||||||
input(
|
|
||||||
id = "password",
|
|
||||||
placeholder = "Password",
|
|
||||||
autoComplete = "off",
|
|
||||||
type = InputType.password,
|
|
||||||
error = validationErrors.find { it.dataPath == ".password" }?.message
|
|
||||||
)
|
|
||||||
checkBoxInput(name = "checked") {
|
|
||||||
attributes["required"] = ""
|
|
||||||
+" Do you want to proceed ?"
|
|
||||||
}
|
|
||||||
button(type = submit, classes = "block mt-4 btn btn-red", name = "delete") { +"I'm sure" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class SUMMARY(consumer: TagConsumer<*>) :
|
|
||||||
HTMLTag(
|
|
||||||
"summary", consumer, emptyMap(),
|
|
||||||
inlineTag = true,
|
|
||||||
emptyTag = false
|
|
||||||
),
|
|
||||||
HtmlInlineTag
|
|
||||||
|
|
||||||
fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) {
|
|
||||||
SUMMARY(consumer).visit(block)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,5 +4,5 @@ rm app/src/main/resources/css-manifest.json
|
|||||||
rm app/src/main/resources/static/styles*
|
rm app/src/main/resources/static/styles*
|
||||||
|
|
||||||
yarn --cwd css run css-purge \
|
yarn --cwd css run css-purge \
|
||||||
&& docker build -t hubv/simplenotes . \
|
&& docker build -t hubv/simplenotes:latest . \
|
||||||
&& docker push hubv/simplenotes
|
&& docker push hubv/simplenotes:latest
|
||||||
|
|||||||
@ -57,6 +57,10 @@
|
|||||||
<artifactId>owasp-java-html-sanitizer</artifactId>
|
<artifactId>owasp-java-html-sanitizer</artifactId>
|
||||||
<version>20200615.1</version>
|
<version>20200615.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlinx</groupId>
|
||||||
|
<artifactId>kotlinx-serialization-runtime</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@ -6,6 +6,8 @@ import be.simplenotes.domain.security.PasswordHash
|
|||||||
import be.simplenotes.domain.security.SimpleJwt
|
import be.simplenotes.domain.security.SimpleJwt
|
||||||
import be.simplenotes.domain.usecases.NoteService
|
import be.simplenotes.domain.usecases.NoteService
|
||||||
import be.simplenotes.domain.usecases.UserService
|
import be.simplenotes.domain.usecases.UserService
|
||||||
|
import be.simplenotes.domain.usecases.export.ExportUseCase
|
||||||
|
import be.simplenotes.domain.usecases.export.ExportUseCaseImpl
|
||||||
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
|
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
|
||||||
import be.simplenotes.domain.usecases.markdown.MarkdownConverterImpl
|
import be.simplenotes.domain.usecases.markdown.MarkdownConverterImpl
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
|
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
|
||||||
@ -20,10 +22,11 @@ val domainModule = module {
|
|||||||
single<LoginUseCase> { LoginUseCaseImpl(get(), get(), get()) }
|
single<LoginUseCase> { LoginUseCaseImpl(get(), get(), get()) }
|
||||||
single<RegisterUseCase> { RegisterUseCaseImpl(get(), get()) }
|
single<RegisterUseCase> { RegisterUseCaseImpl(get(), get()) }
|
||||||
single<DeleteUseCase> { DeleteUseCaseImpl(get(), get()) }
|
single<DeleteUseCase> { DeleteUseCaseImpl(get(), get()) }
|
||||||
single { UserService(get(), get(), get()) }
|
single { UserService(get(), get(), get(), get()) }
|
||||||
single<PasswordHash> { BcryptPasswordHash() }
|
single<PasswordHash> { BcryptPasswordHash() }
|
||||||
single { SimpleJwt(get()) }
|
single { SimpleJwt(get()) }
|
||||||
single { JwtPayloadExtractor(get()) }
|
single { JwtPayloadExtractor(get()) }
|
||||||
single { NoteService(get(), get()) }
|
single { NoteService(get(), get()) }
|
||||||
single<MarkdownConverter> { MarkdownConverterImpl() }
|
single<MarkdownConverter> { MarkdownConverterImpl() }
|
||||||
|
single<ExportUseCase> { ExportUseCaseImpl(get()) }
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
package be.simplenotes.domain.model
|
package be.simplenotes.domain.model
|
||||||
|
|
||||||
|
import kotlinx.serialization.Contextual
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
import java.time.LocalDateTime
|
import java.time.LocalDateTime
|
||||||
import java.util.*
|
import java.util.*
|
||||||
|
|
||||||
@ -28,3 +30,13 @@ data class PersistedNote(
|
|||||||
val updatedAt: LocalDateTime,
|
val updatedAt: LocalDateTime,
|
||||||
val uuid: UUID,
|
val uuid: UUID,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ExportedNote(
|
||||||
|
val title: String,
|
||||||
|
val tags: List<String>,
|
||||||
|
val markdown: String,
|
||||||
|
val html: String,
|
||||||
|
@Contextual val updatedAt: LocalDateTime,
|
||||||
|
val trash: Boolean,
|
||||||
|
)
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package be.simplenotes.domain.usecases
|
package be.simplenotes.domain.usecases
|
||||||
|
|
||||||
|
import be.simplenotes.domain.usecases.export.ExportUseCase
|
||||||
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
|
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
|
||||||
import be.simplenotes.domain.usecases.users.login.LoginUseCase
|
import be.simplenotes.domain.usecases.users.login.LoginUseCase
|
||||||
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
|
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
|
||||||
@ -8,4 +9,8 @@ class UserService(
|
|||||||
loginUseCase: LoginUseCase,
|
loginUseCase: LoginUseCase,
|
||||||
registerUseCase: RegisterUseCase,
|
registerUseCase: RegisterUseCase,
|
||||||
deleteUseCase: DeleteUseCase,
|
deleteUseCase: DeleteUseCase,
|
||||||
) : LoginUseCase by loginUseCase, RegisterUseCase by registerUseCase, DeleteUseCase by deleteUseCase
|
exportUseCase: ExportUseCase,
|
||||||
|
) : LoginUseCase by loginUseCase,
|
||||||
|
RegisterUseCase by registerUseCase,
|
||||||
|
DeleteUseCase by deleteUseCase,
|
||||||
|
ExportUseCase by exportUseCase
|
||||||
|
|||||||
5
domain/src/main/kotlin/usecases/export/ExportUseCase.kt
Normal file
5
domain/src/main/kotlin/usecases/export/ExportUseCase.kt
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package be.simplenotes.domain.usecases.export
|
||||||
|
|
||||||
|
interface ExportUseCase {
|
||||||
|
fun export(userId: Int): String
|
||||||
|
}
|
||||||
43
domain/src/main/kotlin/usecases/export/ExportUseCaseImpl.kt
Normal file
43
domain/src/main/kotlin/usecases/export/ExportUseCaseImpl.kt
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package be.simplenotes.domain.usecases.export
|
||||||
|
|
||||||
|
import be.simplenotes.domain.model.ExportedNote
|
||||||
|
import be.simplenotes.domain.usecases.repositories.NoteRepository
|
||||||
|
import kotlinx.serialization.KSerializer
|
||||||
|
import kotlinx.serialization.builtins.ListSerializer
|
||||||
|
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 java.time.LocalDateTime
|
||||||
|
|
||||||
|
internal class ExportUseCaseImpl(private val noteRepository: NoteRepository) : ExportUseCase {
|
||||||
|
override fun export(userId: Int): String {
|
||||||
|
val module = SerializersModule {
|
||||||
|
contextual(LocalDateTime::class, LocalDateTimeSerializer)
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
serializersModule = module
|
||||||
|
}
|
||||||
|
|
||||||
|
val notes = noteRepository.export(userId)
|
||||||
|
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,5 +1,6 @@
|
|||||||
package be.simplenotes.domain.usecases.repositories
|
package be.simplenotes.domain.usecases.repositories
|
||||||
|
|
||||||
|
import be.simplenotes.domain.model.ExportedNote
|
||||||
import be.simplenotes.domain.model.Note
|
import be.simplenotes.domain.model.Note
|
||||||
import be.simplenotes.domain.model.PersistedNote
|
import be.simplenotes.domain.model.PersistedNote
|
||||||
import be.simplenotes.domain.model.PersistedNoteMetadata
|
import be.simplenotes.domain.model.PersistedNoteMetadata
|
||||||
@ -25,4 +26,5 @@ interface NoteRepository {
|
|||||||
fun create(userId: Int, note: Note): PersistedNote
|
fun create(userId: Int, note: Note): PersistedNote
|
||||||
fun find(userId: Int, uuid: UUID): PersistedNote?
|
fun find(userId: Int, uuid: UUID): PersistedNote?
|
||||||
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
|
fun update(userId: Int, uuid: UUID, note: Note): PersistedNote?
|
||||||
|
fun export(userId: Int): List<ExportedNote>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package be.simplenotes.persistance.notes
|
package be.simplenotes.persistance.notes
|
||||||
|
|
||||||
|
import be.simplenotes.domain.model.ExportedNote
|
||||||
import be.simplenotes.domain.model.Note
|
import be.simplenotes.domain.model.Note
|
||||||
import be.simplenotes.domain.model.PersistedNote
|
import be.simplenotes.domain.model.PersistedNote
|
||||||
import be.simplenotes.domain.model.PersistedNoteMetadata
|
import be.simplenotes.domain.model.PersistedNoteMetadata
|
||||||
@ -167,4 +168,33 @@ internal class NoteRepositoryImpl(private val db: Database) : NoteRepository {
|
|||||||
(it.name eq tag) and (it.note.userId eq userId) and (it.note.deleted eq deleted)
|
(it.name eq tag) and (it.note.userId eq userId) and (it.note.deleted eq deleted)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun export(userId: Int): List<ExportedNote> {
|
||||||
|
|
||||||
|
val notes = db.notes
|
||||||
|
.filterColumns { it.columns - it.userId }
|
||||||
|
.filter { it.userId eq userId }
|
||||||
|
.sortedByDescending { it.updatedAt }
|
||||||
|
.toList()
|
||||||
|
|
||||||
|
if (notes.isEmpty()) return emptyList()
|
||||||
|
|
||||||
|
val uuids = notes.map { note -> note.uuid }
|
||||||
|
|
||||||
|
val tagsByUuid = db.tags
|
||||||
|
.filterColumns { listOf(it.noteUuid, it.name) }
|
||||||
|
.filter { it.noteUuid inList uuids }
|
||||||
|
.groupByTo(HashMap(), { it.note.uuid }, { it.name })
|
||||||
|
|
||||||
|
return notes.map { note ->
|
||||||
|
ExportedNote(
|
||||||
|
title = note.title,
|
||||||
|
tags = tagsByUuid[note.uuid] ?: emptyList(),
|
||||||
|
markdown = note.markdown,
|
||||||
|
html = note.html,
|
||||||
|
updatedAt = note.updatedAt,
|
||||||
|
trash = note.deleted,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
19
pom.xml
19
pom.xml
@ -149,7 +149,17 @@
|
|||||||
<arg>-Xno-call-assertions</arg>
|
<arg>-Xno-call-assertions</arg>
|
||||||
<arg>-Xno-receiver-assertions</arg>
|
<arg>-Xno-receiver-assertions</arg>
|
||||||
</args>
|
</args>
|
||||||
|
<compilerPlugins>
|
||||||
|
<plugin>kotlinx-serialization</plugin>
|
||||||
|
</compilerPlugins>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-serialization</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
@ -182,6 +192,11 @@
|
|||||||
<artifactId>kotlin-reflect</artifactId>
|
<artifactId>kotlin-reflect</artifactId>
|
||||||
<version>${kotlin.version}</version>
|
<version>${kotlin.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlinx</groupId>
|
||||||
|
<artifactId>kotlinx-serialization-runtime</artifactId>
|
||||||
|
<version>1.0-M1-1.4.0-rc</version>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
@ -194,6 +209,10 @@
|
|||||||
<id>arrow</id>
|
<id>arrow</id>
|
||||||
<url>https://dl.bintray.com/arrow-kt/arrow-kt/</url>
|
<url>https://dl.bintray.com/arrow-kt/arrow-kt/</url>
|
||||||
</repository>
|
</repository>
|
||||||
|
<repository>
|
||||||
|
<id>kotlinx</id>
|
||||||
|
<url>https://kotlin.bintray.com/kotlinx</url>
|
||||||
|
</repository>
|
||||||
</repositories>
|
</repositories>
|
||||||
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user