24 Commits

Author SHA1 Message Date
hubert e5a2b8993f Remove module-infos
Some libraires are not yet ready..

{Arrow,Lucene,Http4k} should be packed inside a shaded jar because they
all have some split packages

HikariCP has an invalid module-info.java

Kapt doesn't work
2020-10-23 23:20:58 +02:00
hubert 38750a588c Move packages + remove circular dependencies 2020-10-23 23:20:58 +02:00
hubert ee026ec829 Move junit config to simplenotes-test-resources 2020-10-23 16:36:44 +02:00
hubert 29b024d360 Move ArrowAssertions to simplenotes-domain 2020-10-23 16:30:43 +02:00
hubert cd12d1561a Move config into simplenotes-config module 2020-10-23 16:24:50 +02:00
hubert c2eaf3d0cc Move types into simplenotes-types module 2020-10-23 16:12:40 +02:00
hubert 4c9ac8944e Prefix maven modules 2020-10-23 15:45:28 +02:00
hubert 4ff97044f0 Change public/private button css 2020-10-23 08:26:03 +02:00
hubert ead1932d48 Fix some css 2020-10-23 07:16:07 +02:00
hubert 4a7dcec363 Use Json lenses 2020-10-23 06:22:44 +02:00
hubert cb76a3253d Use mapstruct 2020-10-21 22:55:36 +02:00
hubert 681fd635b3 Add health check 2020-10-21 16:30:42 +02:00
hubert 9467db2382 Use transactions at the http layer 2020-10-20 23:26:20 +02:00
hubert 7ed3494808 Clean pom common dependencies 2020-10-20 19:21:29 +02:00
hubert ceb310bf02 Fix favicon extension 2020-10-20 16:10:13 +02:00
hubert 2c3106c5c1 Clean homepage html 2020-10-20 15:56:24 +02:00
hubert 4effa8231a Change icons 2020-10-20 15:55:11 +02:00
hubert b78420e106 Update some dependencies 2020-10-17 03:39:44 +02:00
hubert dd08763161 We don't need that much RAM 2020-09-30 18:57:46 +02:00
hubert e0b1514965 Unload bootstraping modules after startup 2020-09-30 18:56:52 +02:00
hubert 69c91ec86a Proper shutdown 2020-09-30 00:18:09 +02:00
hubert 1bc45461c3 Small refactor 2020-09-30 00:10:37 +02:00
hubert 0dfb2a7e03 Add zip export 2020-09-28 19:21:53 +02:00
hubert a7c8e63b11 Add radio input in order to select the export format 2020-09-28 18:29:07 +02:00
159 changed files with 1933 additions and 1135 deletions
+2 -2
View File
@@ -125,8 +125,8 @@ data/
letsencrypt/ letsencrypt/
# generated resources # generated resources
app/src/main/resources/css-manifest.json simplenotes-app/src/main/resources/css-manifest.json
app/src/main/resources/static/styles* simplenotes-app/src/main/resources/static/styles*
# h2 db # h2 db
*.db *.db
+18 -12
View File
@@ -4,19 +4,23 @@ WORKDIR /tmp
# Cache dependencies # Cache dependencies
COPY pom.xml . COPY pom.xml .
COPY app/pom.xml app/pom.xml COPY simplenotes-test-resources/pom.xml simplenotes-test-resources/pom.xml
COPY domain/pom.xml domain/pom.xml COPY simplenotes-types/pom.xml simplenotes-types/pom.xml
COPY persistance/pom.xml persistance/pom.xml COPY simplenotes-config/pom.xml simplenotes-config/pom.xml
COPY shared/pom.xml shared/pom.xml COPY simplenotes-persistance/pom.xml simplenotes-persistance/pom.xml
COPY search/pom.xml search/pom.xml COPY simplenotes-search/pom.xml simplenotes-search/pom.xml
COPY simplenotes-domain/pom.xml simplenotes-domain/pom.xml
COPY simplenotes-app/pom.xml simplenotes-app/pom.xml
RUN mvn verify clean --fail-never RUN mvn verify clean --fail-never
COPY app/src app/src COPY simplenotes-test-resources/src simplenotes-test-resources/src
COPY domain/src domain/src COPY simplenotes-types/src simplenotes-types/src
COPY persistance/src persistance/src COPY simplenotes-config/src simplenotes-config/src
COPY shared/src shared/src COPY simplenotes-persistance/src simplenotes-persistance/src
COPY search/src search/src COPY simplenotes-search/src simplenotes-search/src
COPY simplenotes-domain/src simplenotes-domain/src
COPY simplenotes-app/src simplenotes-app/src
RUN mvn -Dstyle.color=always package RUN mvn -Dstyle.color=always package
@@ -32,6 +36,8 @@ RUN strip -p --strip-unneeded /myjdk/lib/server/libjvm.so
FROM alpine FROM alpine
RUN apk add --no-cache curl
ENV APPLICATION_USER simplenotes ENV APPLICATION_USER simplenotes
RUN adduser -D -g '' $APPLICATION_USER RUN adduser -D -g '' $APPLICATION_USER
@@ -40,8 +46,8 @@ RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER USER $APPLICATION_USER
COPY --from=builder /tmp/app/target/app-*.jar /app/app.jar COPY --from=builder /tmp/simplenotes-app/target/simplenotes-app-*.jar /app/simplenotes.jar
COPY --from=jdkbuilder /myjdk /myjdk COPY --from=jdkbuilder /myjdk /myjdk
WORKDIR /app WORKDIR /app
CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms256m", "-Xmx1g", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "app.jar"] CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms64m", "-Xmx256m", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "simplenotes.jar"]
-32
View File
@@ -1,32 +0,0 @@
package be.simplenotes.app
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.ConnectorBuilder
import org.http4k.server.Jetty
import org.http4k.server.ServerConfig
import org.http4k.server.asServer
import org.slf4j.LoggerFactory
import be.simplenotes.shared.config.ServerConfig as SimpleNotesServeConfig
class Server(
private val config: SimpleNotesServeConfig,
private val serverConfig: ServerConfig,
private val router: RoutingHttpHandler,
) {
fun start() {
router.asServer(serverConfig).start()
LoggerFactory.getLogger(javaClass).info("Listening on http://${config.host}:${config.port}")
}
}
fun serverConfig(config: SimpleNotesServeConfig): ServerConfig {
val builder: ConnectorBuilder = { server: Server ->
ServerConnector(server).apply {
port = config.port
host = config.host
}
}
return Jetty(config.port, builder)
}
-180
View File
@@ -1,180 +0,0 @@
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
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
import be.simplenotes.app.views.*
import be.simplenotes.domain.domainModule
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.persistance.DbMigrations
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() {
val koin = startKoin {
modules(
persistanceModule,
configModule,
domainModule,
serverModule,
userModule,
baseModule,
noteModule,
settingsModule,
searchModule,
apiModule,
)
}.koin
val dataSourceConfig = koin.get<DataSourceConfig>()
val jwtConfig = koin.get<JwtConfig>()
val serverConfig = koin.get<SimpleNotesServeConfig>()
val logger = LoggerFactory.getLogger("SimpleNotes")
logger.info("datasource: $dataSourceConfig")
logger.info("jwt: $jwtConfig")
logger.info("server: $serverConfig")
val migrations = koin.get<DbMigrations>()
migrations.migrate()
val noteService = koin.get<NoteService>()
noteService.dropAllIndexes()
noteService.indexAll()
koin.get<Server>().start()
}
val serverModule = module {
single { Server(get(), get(), get()) }
single<StaticFileResolver> { StaticFileResolverImpl() }
single {
Router(
get(),
get(),
get(),
get(),
get(),
get(),
requiredAuth = get(AuthType.Required.qualifier),
optionalAuth = get(AuthType.Optional.qualifier),
errorFilter = get(named("ErrorFilter")),
apiAuth = get(named("apiAuthFilter")),
get()
)()
}
single { serverConfig(get()) }
single { RequestContexts() }
single(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get())() }
single(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get())() }
single(named("ErrorFilter")) { ErrorFilter(get())() }
single { ErrorView(get()) }
}
val userModule = module {
single { UserController(get(), get(), get()) }
single { UserView(get()) }
}
val baseModule = module {
single { BaseController(get()) }
single { BaseView(get()) }
}
val noteModule = module {
single { NoteController(get(), get()) }
single { NoteView(get()) }
}
val settingsModule = module {
single { SettingsController(get(), get()) }
single { SettingView(get()) }
}
val configModule = module {
single { Config.dataSourceConfig }
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()
}
}
@@ -1,26 +0,0 @@
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)
@@ -1,17 +0,0 @@
package be.simplenotes.app.extensions
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.FOUND
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
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)
fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
@@ -1,33 +0,0 @@
package be.simplenotes.app.filters
import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.ErrorView
import org.http4k.core.*
import org.slf4j.LoggerFactory
import java.sql.SQLTransientException
class ErrorFilter(private val errorView: ErrorView) {
private val logger = LoggerFactory.getLogger(javaClass)
operator fun invoke(): Filter = Filter { next ->
{
try {
val response = next(it)
if (response.status == Status.NOT_FOUND) Response(Status.NOT_FOUND)
.html(errorView.error(ErrorView.Type.NotFound))
else response
} catch (e: Exception) {
logger.error(e.stackTraceToString())
if (e is SQLTransientException)
Response(Status.SERVICE_UNAVAILABLE).html(errorView.error(ErrorView.Type.SqlTransientError))
.noCache()
else
Response(Status.INTERNAL_SERVER_ERROR).html(errorView.error(ErrorView.Type.Other)).noCache()
} catch (e: NotImplementedError) {
logger.error(e.stackTraceToString())
Response(Status.NOT_IMPLEMENTED).html(errorView.error(ErrorView.Type.Other)).noCache()
}
}
}
}
@@ -1,14 +0,0 @@
package be.simplenotes.app.filters
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Request
object ImmutableFilter {
operator fun invoke() = Filter { next: HttpHandler ->
{ request: Request ->
next(request).header("Cache-Control", "public, max-age=31536000, immutable")
}
}
}
@@ -1,20 +0,0 @@
package be.simplenotes.app.filters
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Request
object SecurityFilter {
operator fun invoke() = Filter { next: HttpHandler ->
{ request: Request ->
val response = next(request)
.header("X-Content-Type-Options", "nosniff")
if (response.header("Content-Type")?.contains("text/html") == true) {
response
.header("Content-Security-Policy", "default-src 'self'")
.header("Referrer-Policy", "no-referrer")
} else response
}
}
}
-115
View File
@@ -1,115 +0,0 @@
package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.*
import kotlinx.html.div
import org.intellij.lang.annotations.Language
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun renderHome(jwtPayload: JwtPayload?) = renderPage(
title = "Home",
description = "A fast and simple note taking website",
jwtPayload = jwtPayload
) {
section("text-center my-2 p-2") {
h1("text-5xl casual") {
span("text-teal-300") { +"Simplenotes " }
+"- access your notes anywhere"
}
}
div("container mx-auto flex flex-wrap justify-center content-center") {
unsafe {
@Language("html")
val html =
"""
<div aria-label="demo" class="md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2">
<div class="flex justify-between mb-4">
<h1 class="text-2xl underline">Notes</h1>
<span>
<span class="btn btn-teal pointer-events-none">Trash (3)</span>
<span class="ml-2 btn btn-green pointer-events-none">New</span>
</span>
</div>
<form class="md:space-x-2" id="search">
<input aria-label="demo-search" name="search" disabled="" value="tag:&quot;demo&quot;">
<span id="buttons">
<button type="button" disabled="" class="btn btn-green pointer-events-none">search</button>
<span class="btn btn-red pointer-events-none">clear</span>
</span>
</form>
<div class="overflow-x-auto">
<table id="notes">
<thead>
<tr>
<th scope="col" class="w-1/2">Title</th>
<th scope="col" class="w-1/4">Updated</th>
<th scope="col" class="w-1/4">Tags</th>
</tr>
</thead>
<tbody>
<tr>
<td><span class="text-blue-200 font-semibold underline">Formula 1</span></td>
<td class="text-center">moments ago</td>
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#demo</span ></li>
</ul>
</td>
</tr>
<tr>
<td><span class="text-blue-200 font-semibold underline">Syntax highlighting</span></td>
<td class="text-center">2 hours ago</td>
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#features</span></li>
<li class="mx-2 my-1"><span class="tag disabled">#demo</span></li>
</ul>
</td>
</tr>
<tr>
<td><span class="text-blue-200 font-semibold underline">report</span></td>
<td class="text-center">5 days ago</td>
<td>
<ul class="inline flex flex-wrap justify-center">
<li class="mx-2 my-1"><span class="tag disabled">#study</span></li>
<li class="mx-2 my-1"><span class="tag disabled">#demo</span></li>
</ul>
</td>
</tr>
</tbody>
</table>
</div>
</div>
""".trimIndent()
+html
}
welcome()
}
}
@Suppress("NOTHING_TO_INLINE")
private inline fun DIV.welcome() {
div("w-full my-auto md:w-1/2 md:order-2 order-1 text-center") {
div("m-4 rounded-lg p-6") {
p("text-teal-400") {
h2("text-3xl text-teal-400 underline") { +"Features:" }
ul("list-disc text-lg list-inside") {
li { +"Markdown support" }
li { +"Full text search" }
li { +"Structured search" }
li { +"Code highlighting" }
li { +"Fast and lightweight" }
li { +"No tracking" }
li { +"Works without javascript" }
li { +"Data export" }
}
}
}
}
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.4 KiB

-1
View File
@@ -1 +0,0 @@
package be.simplenotes.app
+5 -1
View File
@@ -11,7 +11,11 @@ simplenotes.be {
import strict-transport import strict-transport
header -Server header -Server
reverse_proxy http://localhost:8080 reverse_proxy http://localhost:8080 {
health_path /health
health_interval 5s
health_timeout 200ms
}
} }
dev.simplenotes.be { dev.simplenotes.be {
+2 -2
View File
@@ -2,8 +2,8 @@
"name": "css", "name": "css",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "scripts": {
"css": "NODE_ENV=dev MANIFEST=../app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../app/src/main/resources/static/styles.css", "css": "NODE_ENV=dev MANIFEST=../simplenotes-app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../simplenotes-app/src/main/resources/static/styles.css",
"css-purge": "NODE_ENV=production MANIFEST=../app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../app/src/main/resources/static/styles.css" "css-purge": "NODE_ENV=production MANIFEST=../simplenotes-app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../simplenotes-app/src/main/resources/static/styles.css"
}, },
"dependencies": { "dependencies": {
"autoprefixer": "^9.8.6", "autoprefixer": "^9.8.6",
+1 -1
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
purge: { purge: {
content: [ content: [
'../app/src/main/kotlin/views/**/*.kt' '../simplenotes-app/src/main/kotlin/be/simplenotes/app/views/**/*.kt'
] ]
}, },
theme: { theme: {
+2 -2
View File
@@ -1,7 +1,7 @@
#!/bin/sh #!/bin/sh
rm app/src/main/resources/css-manifest.json rm simplenotes-app/src/main/resources/css-manifest.json
rm app/src/main/resources/static/styles* rm simplenotes-app/src/main/resources/static/styles*
yarn --cwd css run css-purge \ yarn --cwd css run css-purge \
&& docker build -t hubv/simplenotes:latest . \ && docker build -t hubv/simplenotes:latest . \
+10 -2
View File
@@ -19,8 +19,10 @@ services:
volumes: volumes:
- notes-db-volume:/var/lib/mysql - notes-db-volume:/var/lib/mysql
healthcheck: healthcheck:
test: ["CMD", "mysqladmin" ,"ping", "-h", "localhost"] test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'"
timeout: 10s interval: 5s
timeout: 1s
start_period: 2s
retries: 10 retries: 10
simplenotes: simplenotes:
@@ -39,6 +41,12 @@ services:
# - PASSWORD # - PASSWORD
ports: ports:
- 127.0.0.1:8080:8080 - 127.0.0.1:8080:8080
healthcheck:
test: "curl --fail -s http://localhost:8080/health"
interval: 5s
timeout: 1s
start_period: 2s
retries: 3
depends_on: depends_on:
db: db:
condition: service_healthy condition: service_healthy
-66
View File
@@ -1,66 +0,0 @@
<project>
<parent>
<artifactId>parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>domain</artifactId>
<dependencies>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.konform</groupId>
<artifactId>konform-jvm</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark</artifactId>
<version>0.62.2</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-ext-gfm-tasklist</artifactId>
<version>0.62.2</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20200713.1</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-runtime</artifactId>
</dependency>
</dependencies>
</project>
@@ -1,5 +0,0 @@
package be.simplenotes.domain.usecases.export
interface ExportUseCase {
fun export(userId: Int): String
}
@@ -1,43 +0,0 @@
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")
}
}
-5
View File
@@ -1,5 +0,0 @@
package be.simplenotes.domain
/**
* Empty file @see [root-package-declaration](https://discuss.kotlinlang.org/t/root-package-declaration-to-reduce-folder-clutter/2247/4)
*/
@@ -1,24 +0,0 @@
package be.simplenotes.persistance
import be.simplenotes.shared.config.DataSourceConfig
import org.flywaydb.core.Flyway
import javax.sql.DataSource
internal class DbMigrationsImpl(
private val dataSource: DataSource,
private val dataSourceConfig: DataSourceConfig
) : DbMigrations {
override fun migrate() {
val migrationDir = when {
dataSourceConfig.jdbcUrl.contains("mariadb") -> "db/migration/mariadb"
else -> "db/migration/other"
}
Flyway.configure()
.dataSource(dataSource)
.locations(migrationDir)
.load()
.migrate()
}
}
@@ -1,36 +0,0 @@
package be.simplenotes.persistance
import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.persistance.notes.NoteRepositoryImpl
import be.simplenotes.persistance.users.UserRepositoryImpl
import be.simplenotes.shared.config.DataSourceConfig
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import me.liuwj.ktorm.database.*
import org.koin.dsl.module
import javax.sql.DataSource
interface DbMigrations {
fun migrate()
}
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
val hikariConfig = HikariConfig().also {
it.jdbcUrl = conf.jdbcUrl
it.driverClassName = conf.driverClassName
it.username = conf.username
it.password = conf.password
it.maximumPoolSize = conf.maximumPoolSize
it.connectionTimeout = conf.connectionTimeout
}
return HikariDataSource(hikariConfig)
}
val persistanceModule = module {
single<UserRepository> { UserRepositoryImpl(get()) }
single<NoteRepository> { NoteRepositoryImpl(get()) }
single<DbMigrations> { DbMigrationsImpl(get(), get()) }
single<DataSource> { hikariDataSource(get()) }
single { Database.connect(get<DataSource>()) }
}
@@ -1,32 +0,0 @@
package be.simplenotes.persistance.users
import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.model.User
import be.simplenotes.domain.usecases.repositories.UserRepository
import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.*
import java.sql.SQLIntegrityConstraintViolationException
internal class UserRepositoryImpl(private val db: Database) : UserRepository {
override fun create(user: User): PersistedUser? {
return try {
db.useTransaction {
val id = db.insertAndGenerateKey(Users) {
it.username to user.username
it.password to user.password
} as Int
PersistedUser(user.username, user.password, id)
}
} catch (e: SQLIntegrityConstraintViolationException) {
null
}
}
override fun find(username: String) = db.users.find { it.username eq username }?.toPersistedUser()
override fun find(id: Int) = db.users.find { it.id eq id }?.toPersistedUser()
override fun exists(username: String) = db.users.any { it.username eq username }
override fun exists(id: Int) = db.users.any { it.id eq id }
override fun delete(id: Int) = db.useTransaction { db.delete(Users) { it.id eq id } == 1 }
override fun findAll() = db.from(Users).select(Users.id).map { it[Users.id]!! }
}
-1
View File
@@ -1 +0,0 @@
package be.simplenotes.persistance
+142 -112
View File
@@ -1,23 +1,27 @@
<project> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>parent</artifactId> <artifactId>simplenotes-parent</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<modules> <modules>
<module>persistance</module> <module>simplenotes-persistance</module>
<module>app</module> <module>simplenotes-app</module>
<module>domain</module> <module>simplenotes-domain</module>
<module>shared</module> <module>simplenotes-search</module>
<module>search</module> <module>simplenotes-types</module>
<module>simplenotes-config</module>
<module>simplenotes-test-resources</module>
</modules> </modules>
<packaging>pom</packaging> <packaging>pom</packaging>
<properties> <properties>
<java.version>14</java.version> <java.version>14</java.version>
<kotlin.version>1.4.0</kotlin.version> <kotlin.version>1.4.10</kotlin.version>
<junit.version>5.6.2</junit.version> <junit.version>5.6.2</junit.version>
<kotlin.code.style>official</kotlin.code.style> <kotlin.code.style>official</kotlin.code.style>
@@ -27,6 +31,8 @@
<maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target> <maven.compiler.target>${java.version}</maven.compiler.target>
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget> <kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
</properties> </properties>
<dependencies> <dependencies>
@@ -35,101 +41,83 @@
<artifactId>kotlin-stdlib-jdk8</artifactId> <artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
</dependency> </dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<version>0.10.5</version>
</dependency>
<!-- region tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-test-junit</artifactId>
<version>${kotlin.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.10.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-test</artifactId>
<version>2.1.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<version>1.7.0.3</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.16.1</version>
<scope>test</scope>
</dependency>
<!-- endregion -->
</dependencies> </dependencies>
<build> <build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory> <sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory> <testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId>
<version>3.0.0-M5</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</pluginManagement>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId> <artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M4</version> <version>3.0.0-M3</version>
<dependencies> <executions>
<dependency> <execution>
<groupId>org.apache.maven.surefire</groupId> <id>enforce</id>
<artifactId>surefire-junit-platform</artifactId> <goals>
<version>3.0.0-M4</version> <goal>enforce</goal>
</dependency> </goals>
</dependencies> <configuration>
</plugin> <rules>
<plugin> <banDuplicatePomDependencyVersions/>
<groupId>org.apache.maven.plugins</groupId> <requireMavenVersion>
<artifactId>maven-compiler-plugin</artifactId> <version>3.6</version>
<version>3.8.1</version> </requireMavenVersion>
</plugin> </rules>
<plugin> </configuration>
<groupId>org.apache.maven.plugins</groupId> </execution>
<artifactId>maven-dependency-plugin</artifactId> </executions>
<version>3.1.2</version>
</plugin> </plugin>
<plugin> <plugin>
<artifactId>kotlin-maven-plugin</artifactId> <artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
<executions> <executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution> <execution>
<id>compile</id> <id>compile</id>
<phase>process-sources</phase> <phase>process-sources</phase>
@@ -165,39 +153,81 @@
</plugins> </plugins>
</build> </build>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>org.jetbrains.kotlin</groupId> <groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId> <artifactId>kotlin-bom</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk7</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-common</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib</artifactId>
<version>${kotlin.version}</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-reflect</artifactId>
<version>${kotlin.version}</version> <version>${kotlin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-runtime</artifactId> <artifactId>kotlinx-serialization-json-jvm</artifactId>
<version>1.0-M1-1.4.0-rc</version> <version>1.0.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<version>0.11.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-support-mysql</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- region tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<version>1.7.0.3</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.16.1</version>
</dependency>
<!-- endregion -->
</dependencies> </dependencies>
</dependencyManagement> </dependencyManagement>
-1
View File
@@ -1 +0,0 @@
package be.simplenotes.shared
+66 -14
View File
@@ -1,48 +1,70 @@
<project> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent> <parent>
<artifactId>parent</artifactId> <artifactId>simplenotes-parent</artifactId>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</parent> </parent>
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>app</artifactId> <artifactId>simplenotes-app</artifactId>
<properties> <properties>
<http4k.version>3.258.0</http4k.version> <http4k.version>3.268.0</http4k.version>
</properties> </properties>
<dependencies> <dependencies>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>persistance</artifactId> <artifactId>simplenotes-persistance</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>search</artifactId> <artifactId>simplenotes-search</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>domain</artifactId> <artifactId>simplenotes-domain</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId> <artifactId>simplenotes-config</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-core</artifactId> <artifactId>http4k-core</artifactId>
<version>${http4k.version}</version>
</dependency> </dependency>
<!--
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-server-jetty</artifactId> <artifactId>http4k-server-jetty</artifactId>
<version>${http4k.version}</version>
</dependency> </dependency>
-->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.32.v20200930</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.4.32.v20200930</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-html-jvm</artifactId> <artifactId>kotlinx-html-jvm</artifactId>
@@ -50,7 +72,7 @@
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.jetbrains.kotlinx</groupId> <groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-runtime</artifactId> <artifactId>kotlinx-serialization-json-jvm</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.ocpsoft.prettytime</groupId> <groupId>org.ocpsoft.prettytime</groupId>
@@ -58,9 +80,24 @@
<version>4.0.5.Final</version> <version>4.0.5.Final</version>
</dependency> </dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency> <dependency>
<groupId>be.simplenotes</groupId> <groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId> <artifactId>simplenotes-test-resources</artifactId>
<version>1.0-SNAPSHOT</version> <version>1.0-SNAPSHOT</version>
<type>test-jar</type> <type>test-jar</type>
<scope>test</scope> <scope>test</scope>
@@ -68,17 +105,32 @@
<dependency> <dependency>
<groupId>org.http4k</groupId> <groupId>org.http4k</groupId>
<artifactId>http4k-testing-hamkrest</artifactId> <artifactId>http4k-testing-hamkrest</artifactId>
<version>${http4k.version}</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
</dependency>
</dependencies> </dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-bom</artifactId>
<version>${http4k.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId> <artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
<executions> <executions>
<execution> <execution>
<phase>package</phase> <phase>package</phase>
@@ -1,12 +1,12 @@
package be.simplenotes.app package be.simplenotes.app
import be.simplenotes.shared.config.DataSourceConfig import be.simplenotes.config.DataSourceConfig
import be.simplenotes.shared.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.shared.config.ServerConfig import be.simplenotes.config.ServerConfig
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
object Config { class Config {
//region Config loading //region Config loading
private val properties: Properties = javaClass private val properties: Properties = javaClass
.getResource("/application.properties") .getResource("/application.properties")
@@ -0,0 +1,23 @@
package be.simplenotes.app
import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
class Server(
private val config: SimpleNotesServerConfig,
private val http4kServer: Http4kServer,
) {
private val logger = LoggerFactory.getLogger(javaClass)
fun start(): Server {
http4kServer.start()
logger.info("Listening on http://${config.host}:${config.port}")
return this
}
fun stop() {
logger.info("Stopping server")
http4kServer.close()
}
}
@@ -0,0 +1,31 @@
package be.simplenotes.app
import be.simplenotes.app.extensions.addShutdownHook
import be.simplenotes.app.modules.*
import be.simplenotes.domain.domainModule
import be.simplenotes.persistance.migrationModule
import be.simplenotes.persistance.persistanceModule
import be.simplenotes.search.searchModule
import org.koin.core.context.startKoin
import org.koin.core.context.unloadKoinModules
fun main() {
startKoin {
modules(
serverModule,
persistanceModule,
migrationModule,
configModule,
baseModule,
userModule,
noteModule,
settingsModule,
domainModule,
searchModule,
apiModule,
jsonModule
)
}.addShutdownHook()
unloadKoinModules(listOf(migrationModule, configModule))
}
@@ -1,55 +1,46 @@
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.types.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload 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
@@ -0,0 +1,26 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto
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.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.OK
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) = userService
.login(loginFormLens(request))
.fold(
{ Response(BAD_REQUEST) },
{ tokenLens(Token(it), Response(OK)) }
)
}
@Serializable
data class Token(val token: String)
@@ -0,0 +1,12 @@
package be.simplenotes.app.controllers
import be.simplenotes.persistance.DbHealthCheck
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
class HealthCheckController(private val dbHealthCheck: DbHealthCheck) {
fun healthCheck(request: Request) =
if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
}
@@ -7,10 +7,7 @@ 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.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm import be.simplenotes.domain.usecases.users.delete.DeleteForm
import org.http4k.core.Method import org.http4k.core.*
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.body.form
import org.http4k.core.cookie.invalidateCookie import org.http4k.core.cookie.invalidateCookie
@@ -49,18 +46,29 @@ class SettingsController(
) )
} }
fun export(request: Request, jwtPayload: JwtPayload): Response { private fun attachment(filename: String, contentType: String) = { response: Response ->
val isDownload = request.form("download") != null val name = filename.replace("[^a-zA-Z0-9-_.]".toRegex(), "_")
val json = userService.export(jwtPayload.userId) response
val res = Response(Status.OK).body(json).header("Content-Type", "application/json") .header("Content-Disposition", "attachment; filename=\"$name\"")
return if (isDownload) res.header( .header("Content-Type", contentType)
"Content-Disposition",
"attachment; filename=\"simplenotes-export-${sanitizeFilename(jwtPayload.username)}.json\""
)
else res
} }
private fun sanitizeFilename(inputName: String): String = inputName.replace("[^a-zA-Z0-9-_.]".toRegex(), "_") fun export(request: Request, jwtPayload: JwtPayload): Response {
val isDownload = request.form("download") != null
return if (isDownload) {
val filename = "simplenotes-export-${jwtPayload.username}"
if (request.form("format") == "zip") {
val zip = userService.exportAsZip(jwtPayload.userId)
Response(Status.OK)
.with(attachment("$filename.zip", "application/zip"))
.body(zip)
} else
Response(Status.OK)
.with(attachment("$filename.json", "application/json"))
.body(userService.exportAsJson(jwtPayload.userId))
} else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json")
}
private fun Request.deleteForm(jwtPayload: JwtPayload) = private fun Request.deleteForm(jwtPayload: JwtPayload) =
DeleteForm(jwtPayload.username, form("password"), form("checked") != null) DeleteForm(jwtPayload.username, form("password"), form("checked") != null)
@@ -10,7 +10,7 @@ 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
import be.simplenotes.domain.usecases.users.register.UserExists import be.simplenotes.domain.usecases.users.register.UserExists
import be.simplenotes.shared.config.JwtConfig import be.simplenotes.config.JwtConfig
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
@@ -0,0 +1,35 @@
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.Response
import org.http4k.core.Status.Companion.FOUND
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
import org.http4k.lens.*
fun Response.html(html: String) = body(html)
.header("Content-Type", "text/html; charset=utf-8")
.header("Cache-Control", "no-cache")
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
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) }
)
@@ -0,0 +1,12 @@
package be.simplenotes.app.extensions
import org.koin.core.KoinApplication
import kotlin.concurrent.thread
fun KoinApplication.addShutdownHook() {
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
close()
}
)
}
@@ -19,25 +19,23 @@ class AuthFilter(
private val ctx: RequestContexts, private val ctx: RequestContexts,
private val source: JwtSource = JwtSource.Cookie, private val source: JwtSource = JwtSource.Cookie,
private val redirect: Boolean = true, private val redirect: Boolean = true,
) { ) : Filter {
operator fun invoke() = Filter { next -> override fun invoke(next: HttpHandler): HttpHandler = {
{ val token = when (source) {
val token = when (source) { JwtSource.Header -> it.bearerTokenHeader()
JwtSource.Header -> it.bearerTokenHeader() JwtSource.Cookie -> it.bearerTokenCookie()
JwtSource.Cookie -> it.bearerTokenCookie() }
val jwtPayload = token?.let { token -> extractor(token) }
when {
jwtPayload != null -> {
ctx[it][authKey] = jwtPayload
next(it)
} }
val jwtPayload = token?.let { token -> extractor(token) } authType == AuthType.Required -> {
when { if (redirect) Response.redirect("/login")
jwtPayload != null -> { else Response(UNAUTHORIZED)
ctx[it][authKey] = jwtPayload
next(it)
}
authType == AuthType.Required -> {
if (redirect) Response.redirect("/login")
else Response(UNAUTHORIZED)
}
else -> next(it)
} }
else -> next(it)
} }
} }
} }
@@ -0,0 +1,45 @@
package be.simplenotes.app.filters
import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.ErrorView
import be.simplenotes.app.views.ErrorView.Type.*
import org.http4k.core.*
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import org.slf4j.LoggerFactory
import java.sql.SQLTransientException
class ErrorFilter(private val errorView: ErrorView) : Filter {
private val logger = LoggerFactory.getLogger(javaClass)
private fun errorResponse(status: Status): Response {
val type = when (status) {
SERVICE_UNAVAILABLE -> SqlTransientError
NOT_FOUND -> NotFound
NOT_IMPLEMENTED -> Other
else -> Other
}
return Response(status).html(errorView.error(type)).noCache()
}
override fun invoke(next: HttpHandler): HttpHandler = { request ->
try {
val response = next(request)
if (response.status == NOT_FOUND) errorResponse(NOT_FOUND)
else response
} catch (e: SQLTransientException) {
logger.error(e.stackTraceToString())
errorResponse(SERVICE_UNAVAILABLE)
} catch (e: Exception) {
logger.error(e.stackTraceToString())
errorResponse(INTERNAL_SERVER_ERROR)
} catch (e: NotImplementedError) {
logger.error(e.stackTraceToString())
errorResponse(NOT_IMPLEMENTED)
}
}
}
@@ -0,0 +1,11 @@
package be.simplenotes.app.filters
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Request
object ImmutableFilter : Filter {
override fun invoke(next: HttpHandler) = { request: Request ->
next(request).header("Cache-Control", "public, max-age=31536000, immutable")
}
}
@@ -0,0 +1,18 @@
package be.simplenotes.app.filters
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Request
object SecurityFilter : Filter {
override fun invoke(next: HttpHandler): HttpHandler = { request: Request ->
val response = next(request)
.header("X-Content-Type-Options", "nosniff")
if (response.header("Content-Type")?.contains("text/html") == true) {
response
.header("Content-Security-Policy", "default-src 'self'")
.header("Referrer-Policy", "no-referrer")
} else response
}
}
@@ -0,0 +1,13 @@
package be.simplenotes.app.filters
import me.liuwj.ktorm.database.Database
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
class TransactionFilter(private val db: Database) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = { request ->
db.useTransaction {
next(request)
}
}
}
@@ -0,0 +1,40 @@
package be.simplenotes.app.jetty
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletContextHandler.SESSIONS
import org.eclipse.jetty.servlet.ServletHolder
import org.http4k.core.HttpHandler
import org.http4k.server.Http4kServer
import org.http4k.server.ServerConfig
import org.http4k.servlet.asServlet
class Jetty(private val port: Int, private val server: Server) : ServerConfig {
constructor(port: Int = 8000) : this(port, http(port))
constructor(port: Int, vararg inConnectors: ConnectorBuilder) : this(port, Server().apply {
inConnectors.forEach { addConnector(it(this)) }
})
override fun toServer(httpHandler: HttpHandler): Http4kServer {
server.insertHandler(httpHandler.toJettyHandler())
return object : Http4kServer {
override fun start(): Http4kServer = apply {
server.start()
}
override fun stop(): Http4kServer = apply { server.stop() }
override fun port(): Int = if (port > 0) port else server.uri.port
}
}
}
fun HttpHandler.toJettyHandler() = ServletContextHandler(SESSIONS).apply {
addServlet(ServletHolder(this@toJettyHandler.asServlet()), "/*")
}
typealias ConnectorBuilder = (Server) -> ServerConnector
fun http(httpPort: Int): ConnectorBuilder = { server: Server -> ServerConnector(server).apply { port = httpPort } }
@@ -0,0 +1,24 @@
package be.simplenotes.app.modules
import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.JwtSource
import org.http4k.core.Filter
import org.koin.core.qualifier.named
import org.koin.dsl.module
val apiModule = module {
single { ApiUserController(get(), get()) }
single { ApiNoteController(get(), get()) }
single<Filter>(named("apiAuthFilter")) {
AuthFilter(
extractor = get(),
authType = AuthType.Required,
ctx = get(),
source = JwtSource.Header,
redirect = false
)
}
}
@@ -0,0 +1,11 @@
package be.simplenotes.app.modules
import be.simplenotes.app.Config
import org.koin.dsl.module
val configModule = module {
single { Config() }
single { get<Config>().dataSourceConfig }
single { get<Config>().jwtConfig }
single { get<Config>().serverConfig }
}
@@ -0,0 +1,29 @@
package be.simplenotes.app.modules
import be.simplenotes.app.controllers.*
import be.simplenotes.app.views.BaseView
import be.simplenotes.app.views.NoteView
import be.simplenotes.app.views.SettingView
import be.simplenotes.app.views.UserView
import org.koin.dsl.module
val userModule = module {
single { UserController(get(), get(), get()) }
single { UserView(get()) }
}
val baseModule = module {
single { HealthCheckController(get()) }
single { BaseController(get()) }
single { BaseView(get()) }
}
val noteModule = module {
single { NoteController(get(), get()) }
single { NoteView(get()) }
}
val settingsModule = module {
single { SettingsController(get(), get()) }
single { SettingView(get()) }
}
@@ -0,0 +1,24 @@
package be.simplenotes.app.modules
import be.simplenotes.app.serialization.LocalDateTimeSerializer
import be.simplenotes.app.serialization.UuidSerializer
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.koin.dsl.module
import java.time.LocalDateTime
import java.util.*
val jsonModule = module {
single {
Json {
prettyPrint = true
serializersModule = get()
}
}
single {
SerializersModule {
contextual(LocalDateTime::class, LocalDateTimeSerializer())
contextual(UUID::class, UuidSerializer())
}
}
}
@@ -0,0 +1,63 @@
package be.simplenotes.app.modules
import be.simplenotes.app.Server
import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.jetty.ConnectorBuilder
import be.simplenotes.app.jetty.Jetty
import be.simplenotes.app.routes.Router
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.utils.StaticFileResolverImpl
import be.simplenotes.app.views.ErrorView
import be.simplenotes.config.ServerConfig
import org.eclipse.jetty.server.ServerConnector
import org.http4k.core.Filter
import org.http4k.core.RequestContexts
import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.asServer
import org.koin.core.qualifier.named
import org.koin.core.qualifier.qualifier
import org.koin.dsl.module
import org.koin.dsl.onClose
import org.http4k.server.ServerConfig as Http4kServerConfig
val serverModule = module {
single(createdAtStart = true) { Server(get(), get()).start() } onClose { it?.stop() }
single { get<RoutingHttpHandler>().asServer(get()) }
single<Http4kServerConfig> {
val config = get<ServerConfig>()
val builder: ConnectorBuilder = { server: org.eclipse.jetty.server.Server ->
ServerConnector(server).apply {
port = config.port
host = config.host
}
}
Jetty(config.port, builder)
}
single<StaticFileResolver> { StaticFileResolverImpl(get()) }
single {
Router(
get(),
get(),
get(),
get(),
get(),
get(),
get(),
requiredAuth = get(AuthType.Required.qualifier),
optionalAuth = get(AuthType.Optional.qualifier),
apiAuth = get(named("apiAuthFilter")),
get(),
get(),
get(),
)()
}
single { RequestContexts() }
single<Filter>(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get()) }
single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) }
single { ErrorFilter(get()) }
single { TransactionFilter(get()) }
single { ErrorView(get()) }
}
@@ -2,19 +2,15 @@ package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.controllers.BaseController import be.simplenotes.app.controllers.*
import be.simplenotes.app.controllers.NoteController import be.simplenotes.app.filters.*
import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.filters.ImmutableFilter
import be.simplenotes.app.filters.SecurityFilter
import be.simplenotes.app.filters.jwtPayload
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Method.* import org.http4k.core.Method.*
import org.http4k.filter.ResponseFilters import org.http4k.filter.ResponseFilters.GZip
import org.http4k.filter.ServerFilters.InitialiseRequestContext import org.http4k.filter.ServerFilters.InitialiseRequestContext
import org.http4k.routing.* import org.http4k.routing.*
import org.http4k.routing.ResourceLoader.Companion.Classpath
class Router( class Router(
private val baseController: BaseController, private val baseController: BaseController,
@@ -23,26 +19,26 @@ class Router(
private val settingsController: SettingsController, private val settingsController: SettingsController,
private val apiUserController: ApiUserController, private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController, private val apiNoteController: ApiNoteController,
private val healthCheckController: HealthCheckController,
private val requiredAuth: Filter, private val requiredAuth: Filter,
private val optionalAuth: Filter, private val optionalAuth: Filter,
private val errorFilter: Filter,
private val apiAuth: Filter, private val apiAuth: Filter,
private val errorFilter: ErrorFilter,
private val transactionFilter: TransactionFilter,
private val contexts: RequestContexts, private val contexts: RequestContexts,
) { ) {
operator fun invoke(): RoutingHttpHandler { operator fun invoke(): RoutingHttpHandler {
val resourceLoader = ResourceLoader.Classpath(("/static")) val basicRoutes =
val basicRoutes = routes( routes(
ImmutableFilter().then(static(resourceLoader, "woff2" to ContentType("font/woff2"))), "/health" bind GET to healthCheckController::healthCheck,
) ImmutableFilter.then(static(Classpath("/static"), "woff2" to ContentType("font/woff2")))
)
infix fun PathMethod.public(handler: PublicHandler) = this to { handler(it, it.jwtPayload(contexts)) } val publicRoutes = routes(
infix fun PathMethod.protected(handler: ProtectedHandler) = this to { handler(it, it.jwtPayload(contexts)!!) }
val publicRoutes: RoutingHttpHandler = routes(
"/" bind GET public baseController::index, "/" bind GET public baseController::index,
"/register" bind GET public userController::register, "/register" bind GET public userController::register,
"/register" bind POST public userController::register, "/register" bind POST `public transactional` userController::register,
"/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,
@@ -51,18 +47,18 @@ class Router(
val protectedRoutes = routes( val protectedRoutes = routes(
"/settings" bind GET protected settingsController::settings, "/settings" bind GET protected settingsController::settings,
"/settings" bind POST protected settingsController::settings, "/settings" bind POST transactional settingsController::settings,
"/export" bind POST protected settingsController::export, "/export" bind POST protected settingsController::export,
"/notes" bind GET protected noteController::list, "/notes" bind GET protected noteController::list,
"/notes" bind POST protected noteController::search, "/notes" bind POST protected noteController::search,
"/notes/new" bind GET protected noteController::new, "/notes/new" bind GET protected noteController::new,
"/notes/new" bind POST protected noteController::new, "/notes/new" bind POST transactional noteController::new,
"/notes/trash" bind GET protected noteController::trash, "/notes/trash" bind GET protected noteController::trash,
"/notes/{uuid}" bind GET protected noteController::note, "/notes/{uuid}" bind GET protected noteController::note,
"/notes/{uuid}" bind POST protected noteController::note, "/notes/{uuid}" bind POST transactional noteController::note,
"/notes/{uuid}/edit" bind GET protected noteController::edit, "/notes/{uuid}/edit" bind GET protected noteController::edit,
"/notes/{uuid}/edit" bind POST protected noteController::edit, "/notes/{uuid}/edit" bind POST transactional noteController::edit,
"/notes/deleted/{uuid}" bind POST protected noteController::deleted, "/notes/deleted/{uuid}" bind POST transactional noteController::deleted,
) )
val apiRoutes = routes( val apiRoutes = routes(
@@ -71,10 +67,10 @@ class Router(
val protectedApiRoutes = routes( val protectedApiRoutes = routes(
"/api/notes" bind GET protected apiNoteController::notes, "/api/notes" bind GET protected apiNoteController::notes,
"/api/notes" bind POST protected apiNoteController::createNote, "/api/notes" bind POST transactional apiNoteController::createNote,
"/api/notes/search" bind POST protected apiNoteController::search, "/api/notes/search" bind POST transactional apiNoteController::search,
"/api/notes/{uuid}" bind GET protected apiNoteController::note, "/api/notes/{uuid}" bind GET protected apiNoteController::note,
"/api/notes/{uuid}" bind PUT protected apiNoteController::update, "/api/notes/{uuid}" bind PUT transactional apiNoteController::update,
) )
val routes = routes( val routes = routes(
@@ -87,11 +83,23 @@ class Router(
val globalFilters = errorFilter val globalFilters = errorFilter
.then(InitialiseRequestContext(contexts)) .then(InitialiseRequestContext(contexts))
.then(SecurityFilter()) .then(SecurityFilter)
.then(ResponseFilters.GZip()) .then(GZip())
return globalFilters.then(routes) return globalFilters.then(routes)
} }
private inline infix fun PathMethod.public(crossinline handler: PublicHandler) =
this to { handler(it, it.jwtPayload(contexts)) }
private inline infix fun PathMethod.protected(crossinline handler: ProtectedHandler) =
this to { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.transactional(crossinline handler: ProtectedHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.`public transactional`(crossinline handler: PublicHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) }
} }
private typealias PublicHandler = (Request, JwtPayload?) -> Response private typealias PublicHandler = (Request, JwtPayload?) -> Response
@@ -0,0 +1,22 @@
package be.simplenotes.app.serialization
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 java.time.LocalDateTime
internal class 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")
}
}
@@ -0,0 +1,22 @@
package be.simplenotes.app.serialization
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 java.util.*
internal class 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()
}
}
@@ -1,6 +1,6 @@
package be.simplenotes.app.utils package be.simplenotes.app.utils
import be.simplenotes.domain.usecases.search.SearchTerms import be.simplenotes.search.SearchTerms
private fun innerRegex(name: String) = private fun innerRegex(name: String) =
"""$name:['"](.*?)['"]""".toRegex() """$name:['"](.*?)['"]""".toRegex()
@@ -1,16 +1,17 @@
package be.simplenotes.app.utils package be.simplenotes.app.utils
import kotlinx.serialization.json.* import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
interface StaticFileResolver { interface StaticFileResolver {
fun resolve(name: String): String? fun resolve(name: String): String?
} }
class StaticFileResolverImpl : StaticFileResolver { class StaticFileResolverImpl(json: Json) : StaticFileResolver {
private val mappings: Map<String, String> private val mappings: Map<String, String>
init { init {
val json = Json {}
val manifest = javaClass.getResource("/css-manifest.json").readText() val manifest = javaClass.getResource("/css-manifest.json").readText()
val manifestObject = json.parseToJsonElement(manifest).jsonObject val manifestObject = json.parseToJsonElement(manifest).jsonObject
val keys = manifestObject.keys val keys = manifestObject.keys
@@ -0,0 +1,108 @@
package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.*
import kotlinx.html.ThScope.col
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun renderHome(jwtPayload: JwtPayload?) = renderPage(
title = "Home",
description = "A fast and simple note taking website",
jwtPayload = jwtPayload
) {
section("text-center my-2 p-2") {
h1("text-5xl casual") {
span("text-teal-300") { +"SimpleNotes " }
+"- access your notes anywhere"
}
}
div("container mx-auto flex flex-wrap justify-center content-center") {
div("md:order-1 order-2 flipped p-4 my-10 w-full md:w-1/2") {
attributes["aria-label"] = "demo"
div("flex justify-between mb-4") {
h1("text-2xl underline") { +"Notes" }
span {
span("btn btn-teal pointer-events-none") { +"Trash (3)" }
span("ml-2 btn btn-green pointer-events-none") { +"New" }
}
}
form(classes = "md:space-x-2") {
id = "search"
input {
attributes["aria-label"] = "demo-search"
attributes["name"] = "search"
attributes["disabled"] = ""
attributes["value"] = "tag:\"demo\""
}
span {
id = "buttons"
button(type = ButtonType.button, classes = "btn btn-green pointer-events-none") {
attributes["disabled"] = ""
+"search"
}
span("btn btn-red pointer-events-none") { +"clear" }
}
}
div("overflow-x-auto") {
demoTable()
}
}
welcome()
}
}
@Suppress("NOTHING_TO_INLINE")
private inline fun DIV.demoTable() {
table {
id = "notes"
thead {
tr {
th(scope = col, classes = "w-1/2") { +"Title" }
th(scope = col, classes = "w-1/4") { +"Updated" }
th(scope = col, classes = "w-1/4") { +"Tags" }
}
}
tbody {
listOf(
Triple("Formula 1", "moments ago", arrayOf("#demo")),
Triple("Syntax highlighting", "2 hours ago", arrayOf("#features", "#demo")),
Triple("report", "5 days ago", arrayOf("#study", "#demo")),
).forEach { (title, ago, tags) ->
tr {
td { span("text-blue-200 font-semibold underline") { +title } }
td("text-center") { +ago }
td {
ul("inline flex flex-wrap justify-center") {
tags.forEach { tag ->
li("mx-2 my-1") { span("tag disabled") { +tag } }
}
}
}
}
}
}
}
}
@Suppress("NOTHING_TO_INLINE")
private inline fun DIV.welcome() {
div("w-full my-auto md:w-1/2 md:order-2 order-1 text-center") {
div("m-4 rounded-lg p-6") {
h2("text-3xl text-teal-400 underline") { +"Features:" }
ul("list-disc text-lg list-inside") {
li { +"Markdown support" }
li { +"Full text search" }
li { +"Structured search" }
li { +"Code highlighting" }
li { +"Fast and lightweight" }
li { +"No tracking" }
li { +"Works without javascript" }
li { +"Data export" }
}
}
}
}
}
@@ -2,8 +2,8 @@ package be.simplenotes.app.views
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.* import be.simplenotes.app.views.components.*
import be.simplenotes.domain.model.PersistedNote import be.simplenotes.types.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError import io.konform.validation.ValidationError
import kotlinx.html.* import kotlinx.html.*
@@ -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,36 +165,34 @@ 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 = "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( button(
type = ButtonType.submit, type = ButtonType.submit,
name = if (note.public) "private" else "public", name = if (note.public) "private" else "public",
classes = "btn btn-teal" 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"
) { ) {
if (note.public) +"Private"
+"This note is public, do you want to make it private ?" }
else button(
+"This note is private, do you want to make it public ?" 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(
type = ButtonType.submit,
name = "delete",
classes = "btn btn-red"
) { +"Delete" }
} }
} }
} }
@@ -26,14 +26,32 @@ class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResol
} }
} }
section("m-4 p-4 bg-gray-800 rounded") { section("m-4 p-2 bg-gray-800 rounded flex flex-wrap justify-around items-end") {
p(classes = "mb-4") {
+"Export all my data" form(classes = "m-2", method = FormMethod.post, action = "/export") {
button(name = "display",
classes = "inline btn btn-teal block",
type = submit) { +"Display my data" }
} }
form(method = FormMethod.post, action = "/export") { form(classes = "m-2", method = FormMethod.post, action = "/export") {
button(name = "display", classes = "inline btn btn-teal block", type = submit) { +"Display my data" }
button(name = "download", classes = "inline btn btn-green block ml-2 mt-2", type = submit) { div {
listOf("json", "zip").forEach { format ->
radioInput(name = "format") {
id = format
attributes["value"] = format
if (format == "json") attributes["checked"] = ""
else attributes["class"] = "ml-4"
}
label(classes = "ml-2") {
attributes["for"] = format
+format
}
}
}
button(name = "download", classes = "inline btn btn-green block mt-2", type = submit) {
+"Download my data" +"Download my data"
} }
} }
@@ -31,7 +31,7 @@ abstract class View(staticFileResolver: StaticFileResolver) {
attributes["crossorigin"] = "anonymous" attributes["crossorigin"] = "anonymous"
} }
link(rel = "stylesheet", href = styles) link(rel = "stylesheet", href = styles)
link(rel = "shortcut icon", href = "/favicon.ico", type = "image/x-icon") icons()
scripts.forEach { src -> scripts.forEach { src ->
script(src = src) {} script(src = src) {}
} }
@@ -42,4 +42,15 @@ abstract class View(staticFileResolver: StaticFileResolver) {
} }
} }
} }
@Suppress("NOTHING_TO_INLINE")
private inline fun HEAD.icons() {
link(rel = "apple-touch-icon", href = "/apple-touch-icon.png") { attributes["sizes"] = "180x180" }
link(rel = "icon", href = "/favicon-32x32.png", type = "image/png") { attributes["sizes"] = "32x32" }
link(rel = "icon", href = "/favicon-16x16.png", type = "image/png") { attributes["sizes"] = "16x16" }
link(rel = "manifest", href = "/site.webmanifest")
link(rel = "mask-icon", href = "/safari-pinned-tab.svg") { attributes["color"] = "#2c7a7b" }
meta(name = "msapplication-TileColor", content = "#00aba9")
meta(name = "theme-color", content = "#2c7a7b")
}
} }
@@ -1,7 +1,7 @@
package be.simplenotes.app.views.components package be.simplenotes.app.views.components
import be.simplenotes.app.utils.toTimeAgo import be.simplenotes.app.utils.toTimeAgo
import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ButtonType.submit import kotlinx.html.ButtonType.submit
import kotlinx.html.FormMethod.post import kotlinx.html.FormMethod.post
@@ -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") {
@@ -1,7 +1,7 @@
package be.simplenotes.app.views.components package be.simplenotes.app.views.components
import be.simplenotes.app.utils.toTimeAgo import be.simplenotes.app.utils.toTimeAgo
import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import kotlinx.html.* import kotlinx.html.*
import kotlinx.html.ThScope.col import kotlinx.html.ThScope.col
Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 22 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#00aba9</TileColor>
</tile>
</msapplication>
</browserconfig>
Binary file not shown.

After

Width:  |  Height:  |  Size: 814 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1,33 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="700.000000pt" height="700.000000pt" viewBox="0 0 700.000000 700.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,700.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1255 6993 c-179 -23 -313 -62 -461 -133 -396 -187 -665 -533 -766
-981 l-23 -104 0 -2275 0 -2275 23 -102 c125 -565 530 -970 1095 -1095 l102
-23 2275 0 2275 0 102 23 c565 125 970 530 1095 1095 l23 102 0 2275 0 2275
-23 102 c-125 566 -521 964 -1090 1095 l-97 22 -2250 2 c-1237 1 -2263 -1
-2280 -3z m1024 -1979 c128 -18 287 -70 394 -127 262 -139 448 -395 472 -649
3 -34 8 -78 11 -95 l5 -33 -255 0 -254 0 -6 28 c-2 15 -7 44 -11 65 -27 168
-204 335 -416 393 -94 26 -317 24 -421 -4 -218 -59 -345 -196 -356 -384 -6
-105 16 -173 80 -243 81 -88 227 -148 563 -230 509 -123 742 -228 916 -412
123 -131 170 -248 176 -448 9 -245 -55 -420 -212 -580 -162 -165 -387 -270
-667 -311 -154 -22 -461 -15 -595 15 -280 62 -513 193 -662 373 -72 85 -162
262 -185 358 -8 36 -18 95 -22 133 l-6 67 254 0 255 0 6 -53 c22 -179 135
-332 310 -417 127 -61 195 -74 387 -75 195 0 279 16 399 76 179 89 260 234
231 415 -22 137 -98 231 -243 302 -109 55 -202 84 -447 142 -237 57 -306 76
-427 120 -340 125 -535 303 -600 550 -23 87 -23 271 0 360 87 335 409 595 827
665 108 19 371 18 499 -1z m3170 0 c202 -35 325 -95 453 -224 77 -77 93 -100
141 -201 30 -63 63 -143 72 -179 49 -183 48 -159 52 -1307 l4 -1083 -256 0
-255 0 0 1034 c0 1160 1 1133 -70 1282 -92 192 -259 271 -548 262 -117 -4
-149 -9 -221 -33 -177 -60 -340 -200 -440 -376 l-31 -56 0 -1056 0 -1057 -255
0 -255 0 0 1480 0 1480 239 0 238 0 6 -82 c4 -46 7 -120 7 -165 0 -45 3 -88 6
-97 4 -11 28 8 92 72 163 164 356 267 575 307 103 19 334 18 446 -1z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,19 @@
{
"name": "SimpleNotes",
"short_name": "SimpleNotes",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
@@ -3,7 +3,7 @@ package be.simplenotes.app.filters
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.JwtPayloadExtractor import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.shared.config.JwtConfig import be.simplenotes.config.JwtConfig
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
@@ -27,8 +27,8 @@ internal class AuthFilterTest {
private val simpleJwt = SimpleJwt(jwtConfig) private val simpleJwt = SimpleJwt(jwtConfig)
private val extractor = JwtPayloadExtractor(simpleJwt) private val extractor = JwtPayloadExtractor(simpleJwt)
private val ctx = RequestContexts() private val ctx = RequestContexts()
private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)() private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)
private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)() private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)
private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) } private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) }
@@ -1,6 +1,6 @@
package be.simplenotes.app.utils package be.simplenotes.app.utils
import be.simplenotes.domain.usecases.search.SearchTerms import be.simplenotes.search.SearchTerms
import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource import org.junit.jupiter.params.provider.MethodSource
+15
View File
@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simplenotes-parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-config</artifactId>
</project>
@@ -1,4 +1,4 @@
package be.simplenotes.shared.config package be.simplenotes.config
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
+113
View File
@@ -0,0 +1,113 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simplenotes-parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-domain</artifactId>
<dependencies>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-test-resources</artifactId>
<version>1.0-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.konform</groupId>
<artifactId>konform-jvm</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark</artifactId>
<version>0.62.2</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-ext-gfm-tasklist</artifactId>
<version>0.62.2</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20200713.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-types</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-persistance</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-search</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -26,7 +26,12 @@ val domainModule = module {
single<PasswordHash> { BcryptPasswordHash() } single<PasswordHash> { BcryptPasswordHash() }
single { SimpleJwt(get()) } single { SimpleJwt(get()) }
single { JwtPayloadExtractor(get()) } single { JwtPayloadExtractor(get()) }
single { NoteService(get(), get(), get(), get()) } single {
NoteService(get(), get(), get(), get()).apply {
dropAllIndexes()
indexAll()
}
}
single<MarkdownConverter> { MarkdownConverterImpl() } single<MarkdownConverter> { MarkdownConverterImpl() }
single<ExportUseCase> { ExportUseCaseImpl(get()) } single<ExportUseCase> { ExportUseCaseImpl(get(), get()) }
} }
@@ -2,7 +2,7 @@ package be.simplenotes.domain.security
import org.owasp.html.HtmlPolicyBuilder import org.owasp.html.HtmlPolicyBuilder
object HtmlSanitizer { internal object HtmlSanitizer {
private val htmlPolicy = HtmlPolicyBuilder() private val htmlPolicy = HtmlPolicyBuilder()
.allowElements("a") .allowElements("a")
.allowCommonBlockElements() .allowCommonBlockElements()
@@ -1,6 +1,6 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.domain.model.PersistedUser import be.simplenotes.types.PersistedUser
import com.auth0.jwt.exceptions.JWTVerificationException import com.auth0.jwt.exceptions.JWTVerificationException
data class JwtPayload(val userId: Int, val username: String) { data class JwtPayload(val userId: Int, val username: String) {
@@ -1,6 +1,6 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.shared.config.JwtConfig import be.simplenotes.config.JwtConfig
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
@@ -2,17 +2,16 @@ package be.simplenotes.domain.usecases
import arrow.core.Either import arrow.core.Either
import arrow.core.extensions.fx import arrow.core.extensions.fx
import be.simplenotes.domain.model.Note import be.simplenotes.types.Note
import be.simplenotes.domain.model.PersistedNote import be.simplenotes.types.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.domain.security.HtmlSanitizer import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.domain.usecases.markdown.MarkdownConverter import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
import be.simplenotes.domain.usecases.repositories.NoteRepository import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.domain.usecases.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.domain.usecases.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import be.simplenotes.domain.usecases.search.SearchTerms import be.simplenotes.search.SearchTerms
import kotlinx.serialization.Serializable
import java.util.* import java.util.*
class NoteService( class NoteService(

Some files were not shown because too many files have changed in this diff Show More