Compare commits
49 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 195c7c10ac | |||
| 724fa0483e | |||
| f2bdc8d6c7 | |||
| 5aa2e80c5f | |||
| 235e8b6e3c | |||
| 1e0fe12396 | |||
| 7ad8b7039b | |||
| 204ae7988e | |||
| a4bf998c5b | |||
| 3e1683dfe5 | |||
| 51b682c593 | |||
| ea110d51d3 | |||
| f255064533 | |||
| 761382da23 | |||
| 525e3a4a3f | |||
| 69e50b158f | |||
| 8997433974 | |||
| 909fb482a8 | |||
| 90701dcdce | |||
| 8439782430 | |||
| e6a7af840a | |||
| 568a2c6831 | |||
| c39a20cf96 | |||
| bf56314473 | |||
| 11caff1634 | |||
| b1478fd154 | |||
| dd174a6327 | |||
| 941380ad16 | |||
| 4f395d254d | |||
| 6a43acfd46 | |||
| 78b84dc62a | |||
| 1120bc9350 | |||
| a37254452b | |||
| cd9fdd28e8 | |||
| c3fc6a4e88 | |||
| cb58a4fbe0 | |||
| 64059984d3 | |||
| fdc8d34f82 | |||
| 95ec674eb8 | |||
| ea7be84ec3 | |||
| c709f2b44d | |||
| 7995a0b3e0 | |||
| bfd562bc60 | |||
| 4fb85a52e4 | |||
| e64352f54c | |||
| c2c03e415e | |||
| 0260bea951 | |||
| 8b8dbd6fe5 | |||
| 536c6e7b79 |
@@ -13,5 +13,6 @@ insert_final_newline = true
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
max_line_length = 120
|
||||
disabled_rules = no-wildcard-imports
|
||||
kotlin_imports_layout = idea
|
||||
ktlint_standard_no-wildcard-imports = disabled
|
||||
ktlint_standard_import-ordering = disabled
|
||||
ktlint_standard_multiline-if-else = disabled
|
||||
|
||||
@@ -1,10 +1 @@
|
||||
## can be generated with `openssl rand -base64 32`
|
||||
JWT_SECRET=
|
||||
#
|
||||
## can be generated with `openssl rand -base64 32`
|
||||
MYSQL_ROOT_PASSWORD=
|
||||
#
|
||||
## can be generated with `openssl rand -base64 32`
|
||||
MYSQL_PASSWORD=
|
||||
# password should be the same as mysql_password
|
||||
PASSWORD=
|
||||
|
||||
@@ -1,24 +1,6 @@
|
||||
# Java
|
||||
.mtj.tmp/
|
||||
*.class
|
||||
*.jar
|
||||
*.war
|
||||
*.ear
|
||||
*.nar
|
||||
hs_err_pid*
|
||||
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
pom.xml.bak
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
# Gradle
|
||||
build/
|
||||
.gradle
|
||||
|
||||
# IntelliJ
|
||||
out/
|
||||
@@ -28,11 +10,8 @@ out/
|
||||
*.ipr
|
||||
*.iws
|
||||
|
||||
# Vue
|
||||
node_modules
|
||||
/dist
|
||||
|
||||
# Local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.*.local
|
||||
|
||||
@@ -49,85 +28,13 @@ pids
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Directory for instrumented libs generated by jscoverage/JSCover
|
||||
lib-cov
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage
|
||||
|
||||
# nyc test coverage
|
||||
.nyc_output
|
||||
|
||||
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
|
||||
.grunt
|
||||
|
||||
# Bower dependency directory (https://bower.io/)
|
||||
bower_components
|
||||
|
||||
# node-waf configuration
|
||||
.lock-wscript
|
||||
|
||||
# Compiled binary addons (https://nodejs.org/api/addons.html)
|
||||
build/Release
|
||||
|
||||
# Dependency directories
|
||||
node_modules/
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript v1 declaration files
|
||||
typings/
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Optional REPL history
|
||||
.node_repl_history
|
||||
|
||||
# Output of 'npm pack'
|
||||
*.tgz
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
|
||||
# parcel-bundler cache (https://parceljs.org/)
|
||||
.cache
|
||||
|
||||
# next.js build output
|
||||
.next
|
||||
|
||||
# nuxt.js build output
|
||||
.nuxt
|
||||
|
||||
# Nuxt generate
|
||||
dist
|
||||
|
||||
# vuepress build output
|
||||
.vuepress/dist
|
||||
|
||||
# Serverless directories
|
||||
.serverless
|
||||
|
||||
# IDE / Editor
|
||||
.idea
|
||||
|
||||
# Service worker
|
||||
sw.*
|
||||
*.private.env.json
|
||||
|
||||
# Certificates
|
||||
data/
|
||||
letsencrypt/
|
||||
|
||||
# generated resources
|
||||
simplenotes-app/src/main/resources/css-manifest.json
|
||||
simplenotes-app/src/main/resources/static/styles*
|
||||
|
||||
# h2 db
|
||||
*.db
|
||||
|
||||
@@ -136,3 +43,4 @@ simplenotes-app/src/main/resources/static/styles*
|
||||
|
||||
# python
|
||||
__pycache__
|
||||
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
# Enable auto-env through the sdkman_auto_env config
|
||||
# Add key=value pairs of SDKs to use below
|
||||
java=15-open
|
||||
gradle=6.8-rc-1
|
||||
kotlin=1.4.20
|
||||
@@ -1,34 +1,8 @@
|
||||
FROM maven:3.6.3-jdk-14 as builder
|
||||
|
||||
WORKDIR /tmp
|
||||
|
||||
# Cache dependencies
|
||||
COPY pom.xml .
|
||||
COPY simplenotes-test-resources/pom.xml simplenotes-test-resources/pom.xml
|
||||
COPY simplenotes-types/pom.xml simplenotes-types/pom.xml
|
||||
COPY simplenotes-config/pom.xml simplenotes-config/pom.xml
|
||||
COPY simplenotes-persistance/pom.xml simplenotes-persistance/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
|
||||
|
||||
COPY simplenotes-test-resources/src simplenotes-test-resources/src
|
||||
COPY simplenotes-types/src simplenotes-types/src
|
||||
COPY simplenotes-config/src simplenotes-config/src
|
||||
COPY simplenotes-persistance/src simplenotes-persistance/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
|
||||
|
||||
FROM openjdk:14-alpine as jdkbuilder
|
||||
FROM eclipse-temurin:19-alpine as jdkbuilder
|
||||
|
||||
RUN apk add --no-cache binutils
|
||||
|
||||
ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net
|
||||
ENV MODULES java.base,java.compiler,java.desktop,java.instrument,java.logging,java.management,java.naming,java.scripting,java.security.jgss,java.sql,java.sql.rowset,java.transaction.xa,java.xml,jdk.net,jdk.zipfs
|
||||
|
||||
RUN jlink --output /myjdk --module-path $JAVA_HOME/jmods --add-modules $MODULES --no-header-files --no-man-pages --strip-debug --compress=2
|
||||
|
||||
@@ -38,16 +12,28 @@ FROM alpine
|
||||
|
||||
RUN apk add --no-cache curl
|
||||
|
||||
ENV APPLICATION_USER simplenotes
|
||||
RUN adduser -D -g '' $APPLICATION_USER
|
||||
|
||||
RUN mkdir /app
|
||||
RUN chown -R $APPLICATION_USER /app
|
||||
RUN mkdir /app/data
|
||||
|
||||
USER $APPLICATION_USER
|
||||
|
||||
COPY --from=builder /tmp/simplenotes-app/target/simplenotes-app-*.jar /app/simplenotes.jar
|
||||
COPY --from=jdkbuilder /myjdk /myjdk
|
||||
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar
|
||||
WORKDIR /app
|
||||
|
||||
CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms64m", "-Xmx256m", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "simplenotes.jar"]
|
||||
VOLUME /app/data
|
||||
|
||||
ENV SERVER_HOST 0.0.0.0
|
||||
|
||||
CMD [ \
|
||||
"/myjdk/bin/java", \
|
||||
"--add-opens", \
|
||||
"java.base/java.nio=ALL-UNNAMED", \
|
||||
"-server", \
|
||||
"-XX:+UnlockExperimentalVMOptions", \
|
||||
"-Xms64m", \
|
||||
"-Xmx256m", \
|
||||
"-XX:+UseG1GC", \
|
||||
"-XX:MaxGCPauseMillis=100", \
|
||||
"-XX:+UseStringDeduplication", \
|
||||
"-jar", \
|
||||
"simplenotes.jar" \
|
||||
]
|
||||
|
||||
@@ -15,5 +15,4 @@
|
||||
## Configuration
|
||||
|
||||
The app is configured with environments variables.
|
||||
If no match is found within the env, a default value is read from a properties file in /app/src/main/resources/application.properties.
|
||||
Don't use the default values for secrets ! Every value inside *.env.dist* should be changed.
|
||||
If no match is found within the env, a default value is read from a yaml file in simplenotes-app/src/main/resources/application.yaml.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
import be.simplenotes.micronaut
|
||||
|
||||
plugins {
|
||||
id("be.simplenotes.base")
|
||||
id("be.simplenotes.kotlinx-serialization")
|
||||
id("be.simplenotes.app-shadow")
|
||||
id("be.simplenotes.docker")
|
||||
id("be.simplenotes.micronaut")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(project(":domain"))
|
||||
implementation(project(":views"))
|
||||
implementation(project(":css"))
|
||||
|
||||
implementation(libs.http4k.core)
|
||||
implementation(libs.http4k.multipart)
|
||||
implementation(libs.bundles.jetty)
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
|
||||
implementation(libs.slf4j.api)
|
||||
runtimeOnly(libs.slf4j.logback)
|
||||
|
||||
micronaut()
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
testImplementation(libs.http4k.testing.hamkrest)
|
||||
}
|
||||
|
||||
docker {
|
||||
image = "hubv/simplenotes"
|
||||
tag = "latest"
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<withJansi>true</withJansi>
|
||||
<encoder>
|
||||
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
|
||||
</pattern>
|
||||
@@ -13,4 +12,6 @@
|
||||
<logger name="me.liuwj.ktorm.database" level="INFO"/>
|
||||
<logger name="com.zaxxer.hikari" level="INFO"/>
|
||||
<logger name="org.flywaydb.core" level="INFO"/>
|
||||
<logger name="io.micronaut" level="INFO"/>
|
||||
<logger name="io.micronaut.context.lifecycle" level="INFO"/>
|
||||
</configuration>
|
||||
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.8 KiB |
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 22 KiB |
|
Before Width: | Height: | Size: 6.2 KiB After Width: | Height: | Size: 6.2 KiB |
|
Before Width: | Height: | Size: 814 B After Width: | Height: | Size: 814 B |
|
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 1.1 KiB |
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 15 KiB |
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
|
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
|
Before Width: | Height: | Size: 2.2 KiB After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 4.3 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1,21 +1,27 @@
|
||||
package be.simplenotes.app
|
||||
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.annotation.PreDestroy
|
||||
import jakarta.inject.Singleton
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.slf4j.LoggerFactory
|
||||
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
|
||||
|
||||
@Singleton
|
||||
class Server(
|
||||
private val config: SimpleNotesServerConfig,
|
||||
private val http4kServer: Http4kServer,
|
||||
) {
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
@PostConstruct
|
||||
fun start(): Server {
|
||||
http4kServer.start()
|
||||
logger.info("Listening on http://${config.host}:${config.port}")
|
||||
logger.info("Listening on http://${config.host}:${http4kServer.port()}")
|
||||
return this
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
fun stop() {
|
||||
logger.info("Stopping server")
|
||||
http4kServer.close()
|
||||
@@ -0,0 +1,14 @@
|
||||
package be.simplenotes.app
|
||||
|
||||
import io.micronaut.context.ApplicationContext
|
||||
import java.lang.Runtime.getRuntime
|
||||
|
||||
fun main() {
|
||||
val env = if (System.getenv("ENV") == "dev") "dev" else "prod"
|
||||
val ctx = ApplicationContext.builder()
|
||||
.deduceEnvironment(false)
|
||||
.environments(env)
|
||||
.start()
|
||||
ctx.createBean(Server::class.java)
|
||||
getRuntime().addShutdownHook(Thread { ctx.stop() })
|
||||
}
|
||||
@@ -1,11 +1,10 @@
|
||||
package be.simplenotes.app.api
|
||||
|
||||
import be.simplenotes.app.extensions.auto
|
||||
import be.simplenotes.app.utils.parseSearchTerms
|
||||
import be.simplenotes.domain.NoteService
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.types.PersistedNote
|
||||
import be.simplenotes.types.PersistedNoteMetadata
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.NoteService
|
||||
import kotlinx.serialization.Contextual
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.Json
|
||||
@@ -17,41 +16,48 @@ import org.http4k.core.Status.Companion.OK
|
||||
import org.http4k.lens.Path
|
||||
import org.http4k.lens.uuid
|
||||
import java.util.*
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
class ApiNoteController(private val noteService: NoteService, private val json: Json) {
|
||||
@Singleton
|
||||
class ApiNoteController(
|
||||
json: Json,
|
||||
private val noteService: NoteService,
|
||||
) {
|
||||
|
||||
fun createNote(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun createNote(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val content = noteContentLens(request)
|
||||
return noteService.create(jwtPayload.userId, content).fold(
|
||||
return noteService.create(loggedInUser, content).fold(
|
||||
{ Response(BAD_REQUEST) },
|
||||
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
|
||||
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) },
|
||||
)
|
||||
}
|
||||
|
||||
fun notes(request: Request, jwtPayload: JwtPayload): Response {
|
||||
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
|
||||
fun notes(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val notes = noteService.paginatedNotes(loggedInUser.userId, page = 1).notes
|
||||
return persistedNotesMetadataLens(notes, Response(OK))
|
||||
}
|
||||
|
||||
fun note(request: Request, jwtPayload: JwtPayload): Response =
|
||||
noteService.find(jwtPayload.userId, uuidLens(request))
|
||||
fun note(request: Request, loggedInUser: LoggedInUser): Response =
|
||||
noteService.find(loggedInUser.userId, uuidLens(request))
|
||||
?.let { persistedNoteLens(it, Response(OK)) }
|
||||
?: Response(NOT_FOUND)
|
||||
|
||||
fun update(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun update(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val content = noteContentLens(request)
|
||||
return noteService.update(jwtPayload.userId, uuidLens(request), content).fold({
|
||||
return noteService.update(loggedInUser, uuidLens(request), content).fold(
|
||||
{
|
||||
Response(BAD_REQUEST)
|
||||
}, {
|
||||
},
|
||||
{
|
||||
if (it == null) Response(NOT_FOUND)
|
||||
else Response(OK)
|
||||
})
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun search(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun search(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val query = searchContentLens(request)
|
||||
val terms = parseSearchTerms(query)
|
||||
val notes = noteService.search(jwtPayload.userId, terms)
|
||||
val notes = noteService.search(loggedInUser.userId, query)
|
||||
return persistedNotesMetadataLens(notes, Response(OK))
|
||||
}
|
||||
|
||||
@@ -61,7 +67,6 @@ class ApiNoteController(private val noteService: NoteService, private val json:
|
||||
private val persistedNotesMetadataLens = json.auto<List<PersistedNoteMetadata>>().toLens()
|
||||
private val persistedNoteLens = json.auto<PersistedNote>().toLens()
|
||||
private val uuidLens = Path.uuid().of("uuid")
|
||||
|
||||
}
|
||||
|
||||
@Serializable
|
||||
@@ -71,4 +76,4 @@ data class NoteContent(val content: String)
|
||||
data class UuidContent(@Contextual val uuid: UUID)
|
||||
|
||||
@Serializable
|
||||
data class SearchContent(@Contextual val query: String)
|
||||
data class SearchContent(val query: String)
|
||||
@@ -1,16 +1,21 @@
|
||||
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 be.simplenotes.domain.LoginForm
|
||||
import be.simplenotes.domain.UserService
|
||||
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
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
class ApiUserController(private val userService: UserService, private val json: Json) {
|
||||
@Singleton
|
||||
class ApiUserController(
|
||||
json: Json,
|
||||
private val userService: UserService,
|
||||
) {
|
||||
private val tokenLens = json.auto<Token>().toLens()
|
||||
private val loginFormLens = json.auto<LoginForm>().toLens()
|
||||
|
||||
@@ -18,7 +23,7 @@ class ApiUserController(private val userService: UserService, private val json:
|
||||
.login(loginFormLens(request))
|
||||
.fold(
|
||||
{ Response(BAD_REQUEST) },
|
||||
{ tokenLens(Token(it), Response(OK)) }
|
||||
{ tokenLens(Token(it), Response(OK)) },
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
package be.simplenotes.app.controllers
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.views.BaseView
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class BaseController(private val view: BaseView) {
|
||||
fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) =
|
||||
Response(OK).html(view.renderHome(loggedInUser))
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
package be.simplenotes.app.controllers
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.domain.MarkdownParsingError
|
||||
import be.simplenotes.domain.NoteService
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.views.NoteView
|
||||
import org.http4k.core.Method
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.BAD_REQUEST
|
||||
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
import org.http4k.core.body.form
|
||||
import org.http4k.routing.path
|
||||
import java.util.*
|
||||
import jakarta.inject.Singleton
|
||||
import kotlin.math.abs
|
||||
|
||||
@Singleton
|
||||
class NoteController(
|
||||
private val view: NoteView,
|
||||
private val noteService: NoteService,
|
||||
) {
|
||||
|
||||
fun new(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(loggedInUser))
|
||||
|
||||
val markdownForm = request.form("markdown") ?: ""
|
||||
|
||||
return noteService.create(loggedInUser, markdownForm).fold(
|
||||
{
|
||||
val html = when (it) {
|
||||
MarkdownParsingError.MissingMeta -> view.noteEditor(
|
||||
loggedInUser,
|
||||
error = "Missing note metadata",
|
||||
textarea = markdownForm,
|
||||
)
|
||||
MarkdownParsingError.InvalidMeta -> view.noteEditor(
|
||||
loggedInUser,
|
||||
error = "Invalid note metadata",
|
||||
textarea = markdownForm,
|
||||
)
|
||||
is MarkdownParsingError.ValidationError -> view.noteEditor(
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
textarea = markdownForm,
|
||||
)
|
||||
}
|
||||
Response(BAD_REQUEST).html(html)
|
||||
},
|
||||
{
|
||||
Response.redirect("/notes/${it.uuid}")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun list(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
|
||||
val tag = request.query("tag")
|
||||
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag)
|
||||
val deletedCount = noteService.countDeleted(loggedInUser.userId)
|
||||
return Response(OK).html(view.notes(loggedInUser, notes, currentPage, pages, deletedCount, tag = tag))
|
||||
}
|
||||
|
||||
fun search(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val query = request.form("search") ?: ""
|
||||
val notes = noteService.search(loggedInUser.userId, query)
|
||||
val deletedCount = noteService.countDeleted(loggedInUser.userId)
|
||||
return Response(OK).html(view.search(loggedInUser, notes, query, deletedCount))
|
||||
}
|
||||
|
||||
fun note(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
|
||||
if (request.method == Method.POST) {
|
||||
if (request.form("delete") != null) {
|
||||
return if (noteService.trash(loggedInUser.userId, noteUuid))
|
||||
Response.redirect("/notes") // TODO: flash cookie to show success ?
|
||||
else
|
||||
Response(NOT_FOUND) // TODO: show an error
|
||||
}
|
||||
if (request.form("public") != null) {
|
||||
if (!noteService.makePublic(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
|
||||
} else if (request.form("private") != null) {
|
||||
if (!noteService.makePrivate(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
|
||||
}
|
||||
}
|
||||
|
||||
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = false))
|
||||
}
|
||||
|
||||
fun public(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
val note = noteService.findPublic(noteUuid) ?: return Response(NOT_FOUND)
|
||||
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = true))
|
||||
}
|
||||
|
||||
fun edit(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
|
||||
|
||||
if (request.method == Method.GET) {
|
||||
return Response(OK).html(view.noteEditor(loggedInUser, textarea = note.markdown))
|
||||
}
|
||||
|
||||
val markdownForm = request.form("markdown") ?: ""
|
||||
|
||||
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
|
||||
{
|
||||
val html = when (it) {
|
||||
MarkdownParsingError.MissingMeta -> view.noteEditor(
|
||||
loggedInUser,
|
||||
error = "Missing note metadata",
|
||||
textarea = markdownForm,
|
||||
)
|
||||
MarkdownParsingError.InvalidMeta -> view.noteEditor(
|
||||
loggedInUser,
|
||||
error = "Invalid note metadata",
|
||||
textarea = markdownForm,
|
||||
)
|
||||
is MarkdownParsingError.ValidationError -> view.noteEditor(
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
textarea = markdownForm,
|
||||
)
|
||||
}
|
||||
Response(BAD_REQUEST).html(html)
|
||||
},
|
||||
{
|
||||
Response.redirect("/notes/${note.uuid}")
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun trash(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
|
||||
val tag = request.query("tag")
|
||||
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag, deleted = true)
|
||||
return Response(OK).html(view.trash(loggedInUser, notes, currentPage, pages))
|
||||
}
|
||||
|
||||
fun deleted(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val uuid = request.uuidPath() ?: return Response(NOT_FOUND)
|
||||
return if (request.form("delete") != null)
|
||||
if (noteService.delete(loggedInUser.userId, uuid))
|
||||
Response.redirect("/notes/trash")
|
||||
else
|
||||
Response(NOT_FOUND)
|
||||
else if (noteService.restore(loggedInUser.userId, uuid))
|
||||
Response.redirect("/notes/$uuid")
|
||||
else
|
||||
Response(NOT_FOUND)
|
||||
}
|
||||
|
||||
private fun Request.uuidPath(): UUID? {
|
||||
val uuidPath = path("uuid")!!
|
||||
return try {
|
||||
UUID.fromString(uuidPath)!!
|
||||
} catch (e: IllegalArgumentException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,24 +2,26 @@ package be.simplenotes.app.controllers
|
||||
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.app.views.SettingView
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.UserService
|
||||
import be.simplenotes.domain.usecases.users.delete.DeleteError
|
||||
import be.simplenotes.domain.usecases.users.delete.DeleteForm
|
||||
import be.simplenotes.domain.*
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.views.SettingView
|
||||
import jakarta.inject.Singleton
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.body.form
|
||||
import org.http4k.core.cookie.invalidateCookie
|
||||
|
||||
@Singleton
|
||||
class SettingsController(
|
||||
private val userService: UserService,
|
||||
private val exportService: ExportService,
|
||||
private val importService: ImportService,
|
||||
private val settingView: SettingView,
|
||||
) {
|
||||
fun settings(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
if (request.method == Method.GET)
|
||||
return Response(Status.OK).html(settingView.settings(jwtPayload))
|
||||
return Response(Status.OK).html(settingView.settings(loggedInUser))
|
||||
|
||||
val deleteForm = request.deleteForm(jwtPayload)
|
||||
val deleteForm = request.deleteForm(loggedInUser)
|
||||
val result = userService.delete(deleteForm)
|
||||
|
||||
return result.fold(
|
||||
@@ -28,21 +30,22 @@ class SettingsController(
|
||||
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer")
|
||||
DeleteError.WrongPassword -> Response(Status.OK).html(
|
||||
settingView.settings(
|
||||
jwtPayload,
|
||||
error = "Wrong password"
|
||||
)
|
||||
loggedInUser,
|
||||
error = "Wrong password",
|
||||
),
|
||||
)
|
||||
|
||||
is DeleteError.InvalidForm -> Response(Status.OK).html(
|
||||
settingView.settings(
|
||||
jwtPayload,
|
||||
validationErrors = it.validationErrors
|
||||
)
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
),
|
||||
)
|
||||
}
|
||||
},
|
||||
{
|
||||
Response.redirect("/").invalidateCookie("Bearer")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -53,23 +56,34 @@ class SettingsController(
|
||||
.header("Content-Type", contentType)
|
||||
}
|
||||
|
||||
fun export(request: Request, jwtPayload: JwtPayload): Response {
|
||||
fun export(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val isDownload = request.form("download") != null
|
||||
|
||||
return if (isDownload) {
|
||||
val filename = "simplenotes-export-${jwtPayload.username}"
|
||||
val filename = "simplenotes-export-${loggedInUser.username}"
|
||||
if (request.form("format") == "zip") {
|
||||
val zip = userService.exportAsZip(jwtPayload.userId)
|
||||
val zip = exportService.exportAsZip(loggedInUser.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")
|
||||
.body(exportService.exportAsJson(loggedInUser.userId))
|
||||
} else Response(Status.OK).body(exportService.exportAsJson(loggedInUser.userId)).header(
|
||||
"Content-Type",
|
||||
"application/json",
|
||||
)
|
||||
}
|
||||
|
||||
private fun Request.deleteForm(jwtPayload: JwtPayload) =
|
||||
DeleteForm(jwtPayload.username, form("password"), form("checked") != null)
|
||||
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
|
||||
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
|
||||
|
||||
fun import(request: Request, loggedInUser: LoggedInUser): Response {
|
||||
val form = MultipartFormBody.from(request)
|
||||
val file = form.file("file") ?: return Response(Status.BAD_REQUEST)
|
||||
val json = file.content.bufferedReader().readText()
|
||||
importService.importJson(loggedInUser, json)
|
||||
return Response.redirect("/notes")
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,10 @@ package be.simplenotes.app.controllers
|
||||
import be.simplenotes.app.extensions.html
|
||||
import be.simplenotes.app.extensions.isSecure
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.app.views.UserView
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.usecases.UserService
|
||||
import be.simplenotes.domain.usecases.users.login.*
|
||||
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
|
||||
import be.simplenotes.domain.usecases.users.register.RegisterForm
|
||||
import be.simplenotes.domain.usecases.users.register.UserExists
|
||||
import be.simplenotes.config.JwtConfig
|
||||
import be.simplenotes.domain.*
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.views.UserView
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
@@ -21,15 +17,17 @@ import org.http4k.core.cookie.SameSite
|
||||
import org.http4k.core.cookie.cookie
|
||||
import org.http4k.core.cookie.invalidateCookie
|
||||
import java.util.concurrent.TimeUnit
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class UserController(
|
||||
private val userService: UserService,
|
||||
private val userView: UserView,
|
||||
private val jwtConfig: JwtConfig,
|
||||
) {
|
||||
fun register(request: Request, jwtPayload: JwtPayload?): Response {
|
||||
fun register(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||
if (request.method == GET) return Response(OK).html(
|
||||
userView.register(jwtPayload)
|
||||
userView.register(loggedInUser),
|
||||
)
|
||||
|
||||
val result = userService.register(request.registerForm())
|
||||
@@ -37,30 +35,30 @@ class UserController(
|
||||
return result.fold(
|
||||
{
|
||||
val html = when (it) {
|
||||
UserExists -> userView.register(
|
||||
jwtPayload,
|
||||
error = "User already exists"
|
||||
RegisterError.UserExists -> userView.register(
|
||||
loggedInUser,
|
||||
error = "User already exists",
|
||||
)
|
||||
is InvalidRegisterForm ->
|
||||
is RegisterError.InvalidRegisterForm ->
|
||||
userView.register(
|
||||
jwtPayload,
|
||||
validationErrors = it.validationErrors
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
)
|
||||
}
|
||||
Response(OK).html(html)
|
||||
},
|
||||
{
|
||||
Response.redirect("/login")
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
private fun Request.registerForm() = RegisterForm(form("username"), form("password"))
|
||||
private fun Request.loginForm(): LoginForm = registerForm()
|
||||
|
||||
fun login(request: Request, jwtPayload: JwtPayload?): Response {
|
||||
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
|
||||
if (request.method == GET) return Response(OK).html(
|
||||
userView.login(jwtPayload)
|
||||
userView.login(loggedInUser),
|
||||
)
|
||||
|
||||
val result = userService.login(request.loginForm())
|
||||
@@ -68,27 +66,27 @@ class UserController(
|
||||
return result.fold(
|
||||
{
|
||||
val html = when (it) {
|
||||
Unregistered ->
|
||||
LoginError.Unregistered ->
|
||||
userView.login(
|
||||
jwtPayload,
|
||||
error = "User does not exist"
|
||||
loggedInUser,
|
||||
error = "User does not exist",
|
||||
)
|
||||
WrongPassword ->
|
||||
LoginError.WrongPassword ->
|
||||
userView.login(
|
||||
jwtPayload,
|
||||
error = "Wrong password"
|
||||
loggedInUser,
|
||||
error = "Wrong password",
|
||||
)
|
||||
is InvalidLoginForm ->
|
||||
is LoginError.InvalidLoginForm ->
|
||||
userView.login(
|
||||
jwtPayload,
|
||||
validationErrors = it.validationErrors
|
||||
loggedInUser,
|
||||
validationErrors = it.validationErrors,
|
||||
)
|
||||
}
|
||||
Response(OK).html(html)
|
||||
},
|
||||
{ token ->
|
||||
Response.redirect("/notes").loginCookie(token, request.isSecure())
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -103,8 +101,8 @@ class UserController(
|
||||
httpOnly = true,
|
||||
sameSite = SameSite.Lax,
|
||||
maxAge = validityInSeconds,
|
||||
secure = secure
|
||||
)
|
||||
secure = secure,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -23,13 +23,14 @@ 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
|
||||
ContentType.APPLICATION_JSON.withNoDirectives(),
|
||||
ContentNegotiation.StrictNoDirective,
|
||||
).map(
|
||||
{ it.payload.asString() },
|
||||
{ Body(it) }
|
||||
{ Body(it) },
|
||||
)
|
||||
|
||||
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
|
||||
{ decodeFromString(it) },
|
||||
{ encodeToString(it) }
|
||||
{ encodeToString(it) },
|
||||
)
|
||||
@@ -1,8 +1,8 @@
|
||||
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 be.simplenotes.views.ErrorView
|
||||
import be.simplenotes.views.ErrorView.Type.*
|
||||
import org.http4k.core.*
|
||||
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
|
||||
import org.http4k.core.Status.Companion.NOT_FOUND
|
||||
@@ -10,7 +10,9 @@ import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
|
||||
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
|
||||
import org.slf4j.LoggerFactory
|
||||
import java.sql.SQLTransientException
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ErrorFilter(private val errorView: ErrorView) : Filter {
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
@@ -3,9 +3,13 @@ package be.simplenotes.app.filters
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
|
||||
object ImmutableFilter : Filter {
|
||||
override fun invoke(next: HttpHandler) = { request: Request ->
|
||||
next(request).header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
val res = next(request)
|
||||
if (res.status == OK)
|
||||
res.header("Cache-Control", "public, max-age=31536000, immutable")
|
||||
else res
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package be.simplenotes.app.filters.auth
|
||||
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.cookie.cookie
|
||||
import org.http4k.lens.BiDiLens
|
||||
|
||||
typealias OptionalAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser?>
|
||||
typealias RequiredAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser>
|
||||
|
||||
enum class JwtSource {
|
||||
Header, Cookie
|
||||
}
|
||||
|
||||
fun Request.bearerTokenCookie(): String? = cookie("Bearer")
|
||||
?.value
|
||||
?.trim()
|
||||
|
||||
fun Request.bearerTokenHeader(): String? =
|
||||
header("Authorization")
|
||||
?.trim()
|
||||
?.takeIf { it.startsWith("Bearer") }
|
||||
?.substringAfter("Bearer")
|
||||
?.trim()
|
||||
@@ -0,0 +1,23 @@
|
||||
package be.simplenotes.app.filters.auth
|
||||
|
||||
import be.simplenotes.app.filters.auth.JwtSource.Cookie
|
||||
import be.simplenotes.domain.security.SimpleJwt
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.core.with
|
||||
|
||||
class OptionalAuthFilter(
|
||||
private val simpleJwt: SimpleJwt<LoggedInUser>,
|
||||
private val lens: OptionalAuthLens,
|
||||
private val source: JwtSource = Cookie,
|
||||
) : Filter {
|
||||
override fun invoke(next: HttpHandler): HttpHandler = {
|
||||
val token = when (source) {
|
||||
JwtSource.Header -> it.bearerTokenHeader()
|
||||
Cookie -> it.bearerTokenCookie()
|
||||
}
|
||||
|
||||
next(it.with(lens of token?.let { simpleJwt.extract(it) }))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package be.simplenotes.app.filters.auth
|
||||
|
||||
import be.simplenotes.app.extensions.redirect
|
||||
import be.simplenotes.domain.security.SimpleJwt
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.Filter
|
||||
import org.http4k.core.HttpHandler
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.UNAUTHORIZED
|
||||
import org.http4k.core.with
|
||||
|
||||
class RequiredAuthFilter(
|
||||
private val simpleJwt: SimpleJwt<LoggedInUser>,
|
||||
private val lens: RequiredAuthLens,
|
||||
private val source: JwtSource = JwtSource.Cookie,
|
||||
private val redirect: Boolean = true,
|
||||
) : Filter {
|
||||
override fun invoke(next: HttpHandler): HttpHandler = {
|
||||
val token = when (source) {
|
||||
JwtSource.Header -> it.bearerTokenHeader()
|
||||
JwtSource.Cookie -> it.bearerTokenCookie()
|
||||
}
|
||||
val jwtPayload = token?.let { simpleJwt.extract(token) }
|
||||
|
||||
if (jwtPayload != null) next(it.with(lens of jwtPayload))
|
||||
else {
|
||||
if (redirect) Response.redirect("/login")
|
||||
else Response(UNAUTHORIZED)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -8,16 +8,18 @@ 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
|
||||
import org.http4k.servlet.jakarta.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 {
|
||||
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())
|
||||
override fun toServer(http: HttpHandler): Http4kServer {
|
||||
server.insertHandler(http.toJettyHandler())
|
||||
|
||||
return object : Http4kServer {
|
||||
override fun start(): Http4kServer = apply {
|
||||
@@ -36,5 +38,3 @@ fun HttpHandler.toJettyHandler() = ServletContextHandler(SESSIONS).apply {
|
||||
}
|
||||
|
||||
typealias ConnectorBuilder = (Server) -> ServerConnector
|
||||
|
||||
fun http(httpPort: Int): ConnectorBuilder = { server: Server -> ServerConnector(server).apply { port = httpPort } }
|
||||
@@ -0,0 +1,47 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.filters.auth.*
|
||||
import be.simplenotes.domain.security.SimpleJwt
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import io.micronaut.context.annotation.Primary
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.http4k.lens.RequestContextKey
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Factory
|
||||
class AuthModule {
|
||||
|
||||
@Singleton
|
||||
@Named("optional")
|
||||
fun optionalAuthLens(ctx: RequestContexts): OptionalAuthLens = RequestContextKey.optional(ctx)
|
||||
|
||||
@Singleton
|
||||
@Named("required")
|
||||
fun requiredAuthLens(ctx: RequestContexts): RequiredAuthLens = RequestContextKey.required(ctx)
|
||||
|
||||
@Singleton
|
||||
fun optionalAuth(simpleJwt: SimpleJwt<LoggedInUser>, @Named("optional") lens: OptionalAuthLens) =
|
||||
OptionalAuthFilter(simpleJwt, lens)
|
||||
|
||||
@Primary
|
||||
@Singleton
|
||||
fun requiredAuth(simpleJwt: SimpleJwt<LoggedInUser>, @Named("required") lens: RequiredAuthLens) =
|
||||
RequiredAuthFilter(simpleJwt, lens)
|
||||
|
||||
@Singleton
|
||||
@Named("api")
|
||||
internal fun apiAuthFilter(
|
||||
simpleJwt: SimpleJwt<LoggedInUser>,
|
||||
@Named("required") lens: RequiredAuthLens,
|
||||
) = RequiredAuthFilter(
|
||||
simpleJwt = simpleJwt,
|
||||
lens = lens,
|
||||
source = JwtSource.Header,
|
||||
redirect = false,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
fun requestContexts() = RequestContexts()
|
||||
}
|
||||
@@ -2,21 +2,20 @@ package be.simplenotes.app.modules
|
||||
|
||||
import be.simplenotes.app.serialization.LocalDateTimeSerializer
|
||||
import be.simplenotes.app.serialization.UuidSerializer
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.modules.SerializersModule
|
||||
import org.koin.dsl.module
|
||||
import java.time.LocalDateTime
|
||||
import java.util.*
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
val jsonModule = module {
|
||||
single {
|
||||
Json {
|
||||
@Factory
|
||||
class JsonModule {
|
||||
|
||||
@Singleton
|
||||
fun json() = Json {
|
||||
prettyPrint = true
|
||||
serializersModule = get()
|
||||
}
|
||||
}
|
||||
single {
|
||||
SerializersModule {
|
||||
serializersModule = SerializersModule {
|
||||
contextual(LocalDateTime::class, LocalDateTimeSerializer())
|
||||
contextual(UUID::class, UuidSerializer())
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package be.simplenotes.app.modules
|
||||
|
||||
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.config.ServerConfig
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import org.eclipse.jetty.server.ServerConnector
|
||||
import org.http4k.server.Http4kServer
|
||||
import org.http4k.server.asServer
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
import org.eclipse.jetty.server.Server as JettyServer
|
||||
import org.http4k.server.ServerConfig as Http4kServerConfig
|
||||
|
||||
@Factory
|
||||
class ServerModule {
|
||||
|
||||
@Singleton
|
||||
@Named("styles")
|
||||
fun styles(resolver: StaticFileResolver) = resolver.resolve("styles.css")!!
|
||||
|
||||
@Singleton
|
||||
fun http4kServer(router: Router, serverConfig: Http4kServerConfig): Http4kServer =
|
||||
router().asServer(serverConfig)
|
||||
|
||||
@Singleton
|
||||
fun http4kServerConfig(config: ServerConfig): Http4kServerConfig {
|
||||
val builder: ConnectorBuilder = { server: JettyServer ->
|
||||
ServerConnector(server).apply {
|
||||
port = config.port
|
||||
host = config.host
|
||||
}
|
||||
}
|
||||
return Jetty(config.port, builder)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.app.api.ApiNoteController
|
||||
import be.simplenotes.app.api.ApiUserController
|
||||
import be.simplenotes.app.filters.auth.RequiredAuthFilter
|
||||
import be.simplenotes.app.filters.auth.RequiredAuthLens
|
||||
import org.http4k.core.Method.*
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.then
|
||||
import org.http4k.routing.PathMethod
|
||||
import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.routes
|
||||
import java.util.function.Supplier
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class ApiRoutes(
|
||||
private val apiUserController: ApiUserController,
|
||||
private val apiNoteController: ApiNoteController,
|
||||
@Named("api") private val auth: RequiredAuthFilter,
|
||||
@Named("required") private val authLens: RequiredAuthLens,
|
||||
) : Supplier<RoutingHttpHandler> {
|
||||
override fun get(): RoutingHttpHandler {
|
||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||
this to { req: Request -> action(req, authLens(req)) }
|
||||
|
||||
return routes(
|
||||
"/login" bind POST to apiUserController::login,
|
||||
|
||||
with(apiNoteController) {
|
||||
auth.then(
|
||||
routes(
|
||||
"/" bind GET to ::notes,
|
||||
"/" bind POST to ::createNote,
|
||||
"/search" bind POST to ::search,
|
||||
"/{uuid}" bind GET to ::note,
|
||||
"/{uuid}" bind PUT to ::update,
|
||||
),
|
||||
).withBasePath("/notes")
|
||||
},
|
||||
|
||||
).withBasePath("/api")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.app.controllers.BaseController
|
||||
import be.simplenotes.app.controllers.NoteController
|
||||
import be.simplenotes.app.controllers.UserController
|
||||
import be.simplenotes.app.filters.ImmutableFilter
|
||||
import be.simplenotes.app.filters.auth.OptionalAuthFilter
|
||||
import be.simplenotes.app.filters.auth.OptionalAuthLens
|
||||
import org.http4k.core.ContentType
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Method.POST
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.then
|
||||
import org.http4k.routing.*
|
||||
import java.util.function.Supplier
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class BasicRoutes(
|
||||
private val baseCtrl: BaseController,
|
||||
private val userCtrl: UserController,
|
||||
private val noteCtrl: NoteController,
|
||||
@Named("optional") private val authLens: OptionalAuthLens,
|
||||
private val auth: OptionalAuthFilter,
|
||||
) : Supplier<RoutingHttpHandler> {
|
||||
|
||||
override fun get(): RoutingHttpHandler {
|
||||
infix fun PathMethod.to(action: PublicHandler) =
|
||||
this to { req: Request -> action(req, authLens(req)) }
|
||||
|
||||
val staticHandler = ImmutableFilter.then(
|
||||
static(
|
||||
ResourceLoader.Classpath("/static"),
|
||||
"woff2" to ContentType("font/woff2"),
|
||||
"webmanifest" to ContentType("application/manifest+json"),
|
||||
),
|
||||
)
|
||||
|
||||
return routes(
|
||||
auth.then(
|
||||
routes(
|
||||
"/" bind GET to baseCtrl::index,
|
||||
"/register" bind GET to userCtrl::register,
|
||||
"/register" bind POST to userCtrl::register,
|
||||
"/login" bind GET to userCtrl::login,
|
||||
"/login" bind POST to userCtrl::login,
|
||||
"/logout" bind POST to userCtrl::logout,
|
||||
"/notes/public/{uuid}" bind GET to noteCtrl::public,
|
||||
),
|
||||
),
|
||||
staticHandler,
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.app.controllers.NoteController
|
||||
import be.simplenotes.app.filters.auth.RequiredAuthFilter
|
||||
import be.simplenotes.app.filters.auth.RequiredAuthLens
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Method.POST
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.then
|
||||
import org.http4k.routing.PathMethod
|
||||
import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.routes
|
||||
import java.util.function.Supplier
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class NoteRoutes(
|
||||
private val noteCtrl: NoteController,
|
||||
private val auth: RequiredAuthFilter,
|
||||
@Named("required") private val authLens: RequiredAuthLens,
|
||||
) : Supplier<RoutingHttpHandler> {
|
||||
override fun get(): RoutingHttpHandler {
|
||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||
this to { req: Request -> action(req, authLens(req)) }
|
||||
|
||||
return auth.then(
|
||||
with(noteCtrl) {
|
||||
routes(
|
||||
"/" bind GET to ::list,
|
||||
"/" bind POST to ::search,
|
||||
"/new" bind GET to ::new,
|
||||
"/new" bind POST to ::new,
|
||||
"/trash" bind GET to ::trash,
|
||||
"/{uuid}" bind GET to ::note,
|
||||
"/{uuid}" bind POST to ::note,
|
||||
"/{uuid}/edit" bind GET to ::edit,
|
||||
"/{uuid}/edit" bind POST to ::edit,
|
||||
"/deleted/{uuid}" bind POST to ::deleted,
|
||||
).withBasePath("/notes")
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.Response
|
||||
|
||||
internal typealias PublicHandler = (Request, LoggedInUser?) -> Response
|
||||
internal typealias ProtectedHandler = (Request, LoggedInUser) -> Response
|
||||
@@ -0,0 +1,31 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.app.filters.ErrorFilter
|
||||
import be.simplenotes.app.filters.SecurityFilter
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.http4k.core.then
|
||||
import org.http4k.filter.ResponseFilters.GZip
|
||||
import org.http4k.filter.ServerFilters.InitialiseRequestContext
|
||||
import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.routing.routes
|
||||
import java.util.function.Supplier
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class Router(
|
||||
private val errorFilter: ErrorFilter,
|
||||
private val contexts: RequestContexts,
|
||||
private val subRouters: List<Supplier<RoutingHttpHandler>>,
|
||||
) {
|
||||
operator fun invoke(): RoutingHttpHandler {
|
||||
val routes = routes(
|
||||
*subRouters.map { it.get() }.toTypedArray(),
|
||||
)
|
||||
|
||||
return errorFilter
|
||||
.then(InitialiseRequestContext(contexts))
|
||||
.then(SecurityFilter)
|
||||
.then(GZip())
|
||||
.then(routes)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package be.simplenotes.app.routes
|
||||
|
||||
import be.simplenotes.app.controllers.SettingsController
|
||||
import be.simplenotes.app.filters.auth.RequiredAuthFilter
|
||||
import be.simplenotes.app.filters.auth.RequiredAuthLens
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Method.POST
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.then
|
||||
import org.http4k.routing.PathMethod
|
||||
import org.http4k.routing.RoutingHttpHandler
|
||||
import org.http4k.routing.bind
|
||||
import org.http4k.routing.routes
|
||||
import java.util.function.Supplier
|
||||
import jakarta.inject.Named
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class SettingsRoutes(
|
||||
private val settingsController: SettingsController,
|
||||
private val auth: RequiredAuthFilter,
|
||||
@Named("required") private val authLens: RequiredAuthLens,
|
||||
) : Supplier<RoutingHttpHandler> {
|
||||
override fun get(): RoutingHttpHandler {
|
||||
infix fun PathMethod.to(action: ProtectedHandler) =
|
||||
this to { req: Request -> action(req, authLens(req)) }
|
||||
|
||||
return auth.then(
|
||||
routes(
|
||||
"/settings" bind GET to settingsController::settings,
|
||||
"/settings" bind POST to settingsController::settings,
|
||||
"/export" bind POST to settingsController::export,
|
||||
"/import" bind POST to settingsController::import,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -17,6 +17,6 @@ internal class LocalDateTimeSerializer : KSerializer<LocalDateTime> {
|
||||
}
|
||||
|
||||
override fun deserialize(decoder: Decoder): LocalDateTime {
|
||||
TODO("Not implemented, isn't needed")
|
||||
return LocalDateTime.parse(decoder.decodeString())
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ import java.util.*
|
||||
|
||||
internal class UuidSerializer : KSerializer<UUID> {
|
||||
override val descriptor: SerialDescriptor
|
||||
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
|
||||
get() = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
|
||||
|
||||
override fun serialize(encoder: Encoder, value: UUID) {
|
||||
encoder.encodeString(value.toString())
|
||||
@@ -3,11 +3,13 @@ package be.simplenotes.app.utils
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
interface StaticFileResolver {
|
||||
fun resolve(name: String): String?
|
||||
}
|
||||
|
||||
@Singleton
|
||||
class StaticFileResolverImpl(json: Json) : StaticFileResolver {
|
||||
private val mappings: Map<String, String>
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
package be.simplenotes.app
|
||||
@@ -1,15 +1,24 @@
|
||||
package be.simplenotes.app.filters
|
||||
|
||||
import be.simplenotes.domain.security.JwtPayload
|
||||
import be.simplenotes.domain.security.JwtPayloadExtractor
|
||||
import be.simplenotes.domain.security.SimpleJwt
|
||||
import be.simplenotes.app.filters.auth.OptionalAuthFilter
|
||||
import be.simplenotes.app.filters.auth.OptionalAuthLens
|
||||
import be.simplenotes.app.filters.auth.RequiredAuthFilter
|
||||
import be.simplenotes.app.filters.auth.RequiredAuthLens
|
||||
import be.simplenotes.config.JwtConfig
|
||||
import be.simplenotes.domain.security.SimpleJwt
|
||||
import be.simplenotes.domain.security.UserJwtMapper
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import com.natpryce.hamkrest.assertion.assertThat
|
||||
import org.http4k.core.*
|
||||
import io.micronaut.context.BeanContext
|
||||
import io.micronaut.inject.qualifiers.Qualifiers
|
||||
import org.http4k.core.Method.GET
|
||||
import org.http4k.core.Request
|
||||
import org.http4k.core.RequestContexts
|
||||
import org.http4k.core.Response
|
||||
import org.http4k.core.Status.Companion.FOUND
|
||||
import org.http4k.core.Status.Companion.OK
|
||||
import org.http4k.core.cookie.cookie
|
||||
import org.http4k.core.then
|
||||
import org.http4k.filter.ServerFilters
|
||||
import org.http4k.hamkrest.hasBody
|
||||
import org.http4k.hamkrest.hasHeader
|
||||
@@ -20,23 +29,37 @@ import org.junit.jupiter.api.Nested
|
||||
import org.junit.jupiter.api.Test
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
internal class AuthFilterTest {
|
||||
internal class RequiredAuthFilterTest {
|
||||
|
||||
// region setup
|
||||
private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS)
|
||||
private val simpleJwt = SimpleJwt(jwtConfig)
|
||||
private val extractor = JwtPayloadExtractor(simpleJwt)
|
||||
private val ctx = RequestContexts()
|
||||
private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)
|
||||
private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)
|
||||
private val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper())
|
||||
|
||||
private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) }
|
||||
private val beanCtx = BeanContext.build()
|
||||
.registerSingleton(jwtConfig)
|
||||
.start()
|
||||
|
||||
private inline fun <reified T> BeanContext.getBean(): T = getBean(T::class.java)
|
||||
private inline fun <reified T> BeanContext.getBean(name: String): T =
|
||||
getBean(T::class.java, Qualifiers.byName(name))
|
||||
|
||||
private val requiredAuth = beanCtx.getBean<RequiredAuthFilter>()
|
||||
private val requiredLens = beanCtx.getBean<RequiredAuthLens>("required")
|
||||
|
||||
private val optionalAuth = beanCtx.getBean<OptionalAuthFilter>()
|
||||
private val optionalLens = beanCtx.getBean<OptionalAuthLens>("optional")
|
||||
|
||||
private val ctx = beanCtx.getBean<RequestContexts>()
|
||||
|
||||
private val app = ServerFilters.InitialiseRequestContext(ctx).then(
|
||||
routes(
|
||||
"/optional" bind GET to optionalAuth.then(echoJwtPayloadHandler),
|
||||
"/protected" bind GET to requiredAuth.then(echoJwtPayloadHandler)
|
||||
)
|
||||
"/optional" bind GET to optionalAuth.then { request: Request ->
|
||||
Response(OK).body(optionalLens(request).toString())
|
||||
},
|
||||
"/protected" bind GET to requiredAuth.then { request: Request ->
|
||||
Response(OK).body(requiredLens(request).toString())
|
||||
},
|
||||
),
|
||||
)
|
||||
// endregion
|
||||
|
||||
@@ -58,7 +81,7 @@ internal class AuthFilterTest {
|
||||
|
||||
@Test
|
||||
fun `it should allow a valid token`() {
|
||||
val jwtPayload = JwtPayload(1, "user")
|
||||
val jwtPayload = LoggedInUser(1, "user")
|
||||
val token = simpleJwt.sign(jwtPayload)
|
||||
val response = app(Request(GET, "/optional").cookie("Bearer", token))
|
||||
assertThat(response, hasStatus(OK))
|
||||
@@ -83,8 +106,8 @@ internal class AuthFilterTest {
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `it should allow a valid token"`() {
|
||||
val jwtPayload = JwtPayload(1, "user")
|
||||
fun `it should allow a valid token`() {
|
||||
val jwtPayload = LoggedInUser(1, "user")
|
||||
val token = simpleJwt.sign(jwtPayload)
|
||||
val response = app(Request(GET, "/protected").cookie("Bearer", token))
|
||||
assertThat(response, hasStatus(OK))
|
||||
@@ -0,0 +1,3 @@
|
||||
plugins {
|
||||
id("be.simplenotes.versions")
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
plugins {
|
||||
`kotlin-dsl`
|
||||
}
|
||||
|
||||
repositories {
|
||||
gradlePluginPortal()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.21")
|
||||
implementation("org.jetbrains.kotlin:kotlin-serialization:1.8.21")
|
||||
implementation("com.github.johnrengelman:shadow:8.1.1")
|
||||
implementation("com.diffplug.spotless:spotless-plugin-gradle:6.13.0")
|
||||
implementation("com.github.ben-manes:gradle-versions-plugin:0.46.0")
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package be.simplenotes
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.kotlin.dsl.create
|
||||
|
||||
open class DockerPluginExtension {
|
||||
var image: String? = null
|
||||
var tag = "latest"
|
||||
}
|
||||
|
||||
class DockerPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
val extension = project.extensions.create<DockerPluginExtension>("docker")
|
||||
|
||||
project.task("dockerBuild") {
|
||||
dependsOn("package")
|
||||
|
||||
group = "docker"
|
||||
description = "Build a docker image"
|
||||
|
||||
doLast {
|
||||
project.exec {
|
||||
commandLine("docker", "build", "-t", "${extension.image}:${extension.tag}", ".")
|
||||
workingDir(project.rootProject.projectDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
project.task("dockerPush") {
|
||||
dependsOn("dockerBuild")
|
||||
|
||||
group = "docker"
|
||||
description = "Push a docker image"
|
||||
|
||||
doLast {
|
||||
project.exec {
|
||||
commandLine("docker", "push", "${extension.image}:${extension.tag}")
|
||||
workingDir(project.rootProject.projectDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
@file:Suppress("SpellCheckingInspection")
|
||||
|
||||
package be.simplenotes
|
||||
|
||||
object Libs {
|
||||
|
||||
object Micronaut {
|
||||
private const val version = "4.0.0-M2"
|
||||
const val inject = "io.micronaut:micronaut-inject:$version"
|
||||
const val processor = "io.micronaut:micronaut-inject-java:$version"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package be.simplenotes
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.artifacts.dsl.DependencyHandler
|
||||
import org.jetbrains.kotlin.gradle.plugin.KaptExtension
|
||||
|
||||
class MicronautPlugin : Plugin<Project> {
|
||||
override fun apply(target: Project) {
|
||||
target.plugins.apply("org.jetbrains.kotlin.kapt")
|
||||
target.extensions.configure<KaptExtension>("kapt") {
|
||||
arguments {
|
||||
arg("micronaut.processing.incremental", true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun DependencyHandler.micronaut() {
|
||||
add("kapt", Libs.Micronaut.processor)
|
||||
add("implementation", Libs.Micronaut.inject)
|
||||
}
|
||||
|
||||
fun DependencyHandler.micronautTest() {
|
||||
add("kaptTest", Libs.Micronaut.processor)
|
||||
add("testImplementation", Libs.Micronaut.inject)
|
||||
}
|
||||
|
||||
fun DependencyHandler.micronautFixtures() {
|
||||
add("kaptTestFixtures", Libs.Micronaut.inject)
|
||||
add("testFixturesImplementation", Libs.Micronaut.processor)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
package be.simplenotes
|
||||
|
||||
import org.gradle.api.Plugin
|
||||
import org.gradle.api.Project
|
||||
import org.gradle.api.plugins.JavaPluginExtension
|
||||
import org.gradle.api.tasks.SourceSetContainer
|
||||
import org.gradle.kotlin.dsl.get
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import org.gradle.kotlin.dsl.register
|
||||
import java.io.File
|
||||
|
||||
class PostcssPlugin : Plugin<Project> {
|
||||
override fun apply(project: Project) {
|
||||
with(project.tasks) {
|
||||
register<PostcssTask>("postcss") {
|
||||
group = "postcss"
|
||||
description = "generate postcss resources"
|
||||
}
|
||||
getByName("processResources").dependsOn("postcss")
|
||||
}
|
||||
|
||||
val sourceSets = project.extensions.getByType<SourceSetContainer>()
|
||||
val root = File("${project.buildDir}/generated-resources/css")
|
||||
sourceSets["main"].resources.srcDir(root)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
package be.simplenotes
|
||||
|
||||
import org.gradle.api.DefaultTask
|
||||
import org.gradle.api.GradleException
|
||||
import org.gradle.api.tasks.*
|
||||
import org.gradle.kotlin.dsl.getByType
|
||||
import java.io.File
|
||||
import java.lang.ProcessBuilder.Redirect.PIPE
|
||||
import java.util.concurrent.TimeUnit
|
||||
import kotlin.concurrent.thread
|
||||
|
||||
open class PostcssTask : DefaultTask() {
|
||||
|
||||
private val viewsProject = project
|
||||
.parent
|
||||
?.project(":views")
|
||||
?: error("Missing :views")
|
||||
|
||||
@get:InputDirectory
|
||||
val templatesDir = viewsProject.extensions
|
||||
.getByType<SourceSetContainer>()
|
||||
.asMap.getOrElse("main") { error("main sources not found") }
|
||||
.allSource.srcDirs
|
||||
.find { it.endsWith("src") }
|
||||
?: error("kotlin sources not found")
|
||||
|
||||
private val yarnRoot = File(project.rootDir, "css")
|
||||
|
||||
@get:InputDirectory
|
||||
val postCssDir = File(project.rootDir, "css/src")
|
||||
|
||||
@get:InputFiles
|
||||
val postCssConfig = listOf(
|
||||
"tailwind.config.js",
|
||||
"postcss.config.js",
|
||||
"package.json"
|
||||
).map { File(yarnRoot, it) }
|
||||
|
||||
@get:OutputDirectory
|
||||
val outputRootDir = File(project.buildDir, "generated-resources/css")
|
||||
|
||||
private val cssIndex = File(postCssDir, "styles.pcss")
|
||||
|
||||
private val cssOutput = File(outputRootDir, "static/styles.css")
|
||||
private val manifestOutput = File(outputRootDir, "css-manifest.json")
|
||||
|
||||
private val purgeGlob = "$templatesDir/**/*.kt"
|
||||
|
||||
@TaskAction
|
||||
fun generateCss() {
|
||||
// TODO: auto yarn install ?
|
||||
|
||||
outputRootDir.deleteRecursively()
|
||||
|
||||
ProcessBuilder("yarn", "run", "postcss", "$cssIndex", "--output", "$cssOutput")
|
||||
.apply {
|
||||
environment().let {
|
||||
it["MANIFEST"] = "$manifestOutput"
|
||||
it["NODE_ENV"] = "production"
|
||||
it["PURGE"] = purgeGlob
|
||||
}
|
||||
}
|
||||
.redirectOutput(PIPE)
|
||||
.redirectError(PIPE)
|
||||
.directory(yarnRoot)
|
||||
.start()
|
||||
.apply {
|
||||
thread { inputStream.use { it.copyTo(System.out) } }
|
||||
thread { errorStream.use { it.copyTo(System.out) } }
|
||||
waitFor(30, TimeUnit.SECONDS)
|
||||
if (exitValue() != 0) throw GradleException(":/")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package be.simplenotes
|
||||
|
||||
import com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar
|
||||
|
||||
plugins {
|
||||
id("com.github.johnrengelman.shadow")
|
||||
}
|
||||
|
||||
tasks.withType<ShadowJar> {
|
||||
|
||||
archiveAppendix.set("with-dependencies")
|
||||
manifest.attributes["Main-Class"] = "be.simplenotes.app.SimpleNotesKt"
|
||||
|
||||
// johnrengelman/shadow#449
|
||||
// we need this for lucene-core
|
||||
manifest.attributes["Multi-Release"] = "true"
|
||||
|
||||
mergeServiceFiles()
|
||||
|
||||
File(rootProject.projectDir, "buildSrc/src/main/resources/exclusions")
|
||||
.listFiles()!!
|
||||
.flatMap {
|
||||
it.readLines()
|
||||
.asSequence()
|
||||
.map { it.trim() }
|
||||
.filterNot { it.isBlank() }
|
||||
.filterNot { it.startsWith("#") }
|
||||
.asIterable()
|
||||
}.forEach { exclude(it) }
|
||||
|
||||
}
|
||||
|
||||
tasks.create("package") {
|
||||
tasks.getByName("build").dependsOn("package")
|
||||
|
||||
dependsOn("shadowJar")
|
||||
|
||||
doLast {
|
||||
println("SimpleNotes Packaged !")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package be.simplenotes
|
||||
|
||||
plugins {
|
||||
id("be.simplenotes.java-convention")
|
||||
id("be.simplenotes.kotlin-convention")
|
||||
id("be.simplenotes.junit-convention")
|
||||
id("com.diffplug.spotless")
|
||||
}
|
||||
|
||||
spotless {
|
||||
kotlin {
|
||||
ktlint("0.48.0").setEditorConfigPath(project.rootProject.file(".editorconfig"))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package be.simplenotes
|
||||
|
||||
plugins {
|
||||
`java-library`
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
group = "be.simplenotes"
|
||||
version = "1.0-SNAPSHOT"
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(19))
|
||||
vendor.set(JvmVendorSpec.ORACLE)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
}
|
||||
|
||||
sourceSets["main"].resources.setSrcDirs(listOf("resources"))
|
||||
sourceSets["main"].java.setSrcDirs(emptyList<String>())
|
||||
sourceSets["test"].resources.setSrcDirs(listOf("testresources"))
|
||||
sourceSets["test"].java.setSrcDirs(emptyList<String>())
|
||||
@@ -0,0 +1,15 @@
|
||||
package be.simplenotes
|
||||
|
||||
plugins {
|
||||
java apply false
|
||||
}
|
||||
|
||||
tasks.withType<Test> {
|
||||
useJUnitPlatform {
|
||||
excludeTags("slow")
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
testRuntimeOnly(project(":junit-config"))
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package be.simplenotes
|
||||
|
||||
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
|
||||
import org.jetbrains.kotlin.gradle.dsl.KotlinVersion
|
||||
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(kotlin("stdlib-jdk8"))
|
||||
implementation(platform(kotlin("bom")))
|
||||
testImplementation(platform(kotlin("bom")))
|
||||
}
|
||||
|
||||
tasks.withType<KotlinCompile> {
|
||||
compilerOptions {
|
||||
jvmTarget.set(JvmTarget.JVM_19)
|
||||
javaParameters.set(true)
|
||||
freeCompilerArgs.addAll(
|
||||
"-Xinline-classes",
|
||||
"-Xno-param-assertions",
|
||||
"-Xno-call-assertions",
|
||||
"-Xno-receiver-assertions",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
kotlin.sourceSets["main"].kotlin.setSrcDirs(listOf("src"))
|
||||
kotlin.sourceSets["test"].kotlin.setSrcDirs(listOf("test"))
|
||||
@@ -0,0 +1,5 @@
|
||||
package be.simplenotes
|
||||
|
||||
plugins {
|
||||
kotlin("plugin.serialization")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package be.simplenotes
|
||||
|
||||
import com.github.benmanes.gradle.versions.updates.DependencyUpdatesTask
|
||||
|
||||
plugins {
|
||||
id("com.github.ben-manes.versions")
|
||||
}
|
||||
|
||||
tasks.named<DependencyUpdatesTask>("dependencyUpdates").configure {
|
||||
resolutionStrategy {
|
||||
componentSelection {
|
||||
all {
|
||||
if ("alpha|beta|rc".toRegex().containsMatchIn(candidate.version.lowercase())) reject("Non stable version")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
implementation-class=be.simplenotes.DockerPlugin
|
||||
@@ -0,0 +1 @@
|
||||
implementation-class=be.simplenotes.MicronautPlugin
|
||||
@@ -0,0 +1 @@
|
||||
implementation-class=be.simplenotes.PostcssPlugin
|
||||
@@ -0,0 +1,16 @@
|
||||
META-INF/maven/**
|
||||
META-INF/proguard/**
|
||||
META-INF/com.android.tools/**
|
||||
META-INF/*.kotlin_module
|
||||
META-INF/DEPENDENCIES*
|
||||
META-INF/NOTICE*
|
||||
META-INF/LICENSE*
|
||||
LICENSE*
|
||||
META-INF/README*
|
||||
META-INF/native-image/**
|
||||
**/module-info.**
|
||||
|
||||
# Jetty
|
||||
about.html
|
||||
jetty-dir.css
|
||||
server-ssl-cert.pem
|
||||
@@ -0,0 +1,261 @@
|
||||
com/google/common/util/**
|
||||
com/google/common/eventbus/**
|
||||
com/google/common/reflect/**
|
||||
com/google/common/escape/**
|
||||
com/google/common/graph/**
|
||||
com/google/common/html/**
|
||||
com/google/common/hash/**
|
||||
com/google/common/xml/**
|
||||
com/google/common/io/**
|
||||
com/google/common/cache/**
|
||||
com/google/common/net/**
|
||||
|
||||
# Collections
|
||||
com/google/common/collect/AbstractBiMap$1.class
|
||||
com/google/common/collect/AbstractBiMap$BiMapEntry.class
|
||||
com/google/common/collect/AbstractBiMap$EntrySet.class
|
||||
com/google/common/collect/AbstractBiMap$Inverse.class
|
||||
com/google/common/collect/AbstractBiMap$KeySet.class
|
||||
com/google/common/collect/AbstractBiMap$ValueSet.class
|
||||
com/google/common/collect/AbstractBiMap.class
|
||||
com/google/common/collect/AbstractSortedKeySortedSetMultimap.class
|
||||
com/google/common/collect/AbstractSortedMultiset$1DescendingMultisetImpl.class
|
||||
com/google/common/collect/AbstractSortedMultiset.class
|
||||
com/google/common/collect/AbstractTable$1.class
|
||||
com/google/common/collect/AbstractTable$CellSet.class
|
||||
com/google/common/collect/AbstractTable$Values.class
|
||||
com/google/common/collect/AbstractTable.class
|
||||
com/google/common/collect/ArrayListMultimap.class
|
||||
com/google/common/collect/ArrayListMultimapGwtSerializationDependencies.class
|
||||
com/google/common/collect/ArrayTable$1.class
|
||||
com/google/common/collect/ArrayTable$2.class
|
||||
com/google/common/collect/ArrayTable$3.class
|
||||
com/google/common/collect/ArrayTable$ArrayMap$1.class
|
||||
com/google/common/collect/ArrayTable$ArrayMap$2.class
|
||||
com/google/common/collect/ArrayTable$ArrayMap.class
|
||||
com/google/common/collect/ArrayTable$Column.class
|
||||
com/google/common/collect/ArrayTable$ColumnMap.class
|
||||
com/google/common/collect/ArrayTable$Row.class
|
||||
com/google/common/collect/ArrayTable$RowMap.class
|
||||
com/google/common/collect/ArrayTable.class
|
||||
com/google/common/collect/ClassToInstanceMap.class
|
||||
com/google/common/collect/CompactHashMap$1.class
|
||||
com/google/common/collect/CompactHashMap$2.class
|
||||
com/google/common/collect/CompactHashMap$3.class
|
||||
com/google/common/collect/CompactHashMap$EntrySetView.class
|
||||
com/google/common/collect/CompactHashMap$Itr.class
|
||||
com/google/common/collect/CompactHashMap$KeySetView.class
|
||||
com/google/common/collect/CompactHashMap$MapEntry.class
|
||||
com/google/common/collect/CompactHashMap$ValuesView.class
|
||||
com/google/common/collect/CompactHashMap.class
|
||||
com/google/common/collect/CompactHashSet$1.class
|
||||
com/google/common/collect/CompactHashSet.class
|
||||
com/google/common/collect/CompactLinkedHashMap$1EntrySetImpl.class
|
||||
com/google/common/collect/CompactLinkedHashMap$1KeySetImpl.class
|
||||
com/google/common/collect/CompactLinkedHashMap$1ValuesImpl.class
|
||||
com/google/common/collect/CompactLinkedHashMap.class
|
||||
com/google/common/collect/CompactLinkedHashSet.class
|
||||
com/google/common/collect/Comparators.class
|
||||
com/google/common/collect/ComputationException.class
|
||||
com/google/common/collect/ConcurrentHashMultiset$1.class
|
||||
com/google/common/collect/ConcurrentHashMultiset$2.class
|
||||
com/google/common/collect/ConcurrentHashMultiset$3.class
|
||||
com/google/common/collect/ConcurrentHashMultiset$EntrySet.class
|
||||
com/google/common/collect/ConcurrentHashMultiset$FieldSettersHolder.class
|
||||
com/google/common/collect/ConcurrentHashMultiset.class
|
||||
com/google/common/collect/DenseImmutableTable$1.class
|
||||
com/google/common/collect/DenseImmutableTable$Column.class
|
||||
com/google/common/collect/DenseImmutableTable$ColumnMap.class
|
||||
com/google/common/collect/DenseImmutableTable$ImmutableArrayMap$1.class
|
||||
com/google/common/collect/DenseImmutableTable$ImmutableArrayMap.class
|
||||
com/google/common/collect/DenseImmutableTable$Row.class
|
||||
com/google/common/collect/DenseImmutableTable$RowMap.class
|
||||
com/google/common/collect/DenseImmutableTable.class
|
||||
com/google/common/collect/DescendingImmutableSortedMultiset.class
|
||||
com/google/common/collect/DescendingMultiset$1EntrySetImpl.class
|
||||
com/google/common/collect/DescendingMultiset.class
|
||||
com/google/common/collect/EnumBiMap.class
|
||||
com/google/common/collect/EnumHashBiMap.class
|
||||
com/google/common/collect/EnumMultiset$1.class
|
||||
com/google/common/collect/EnumMultiset$2$1.class
|
||||
com/google/common/collect/EnumMultiset$2.class
|
||||
com/google/common/collect/EnumMultiset$Itr.class
|
||||
com/google/common/collect/EnumMultiset.class
|
||||
com/google/common/collect/EvictingQueue.class
|
||||
com/google/common/collect/ForwardingBlockingDeque.class
|
||||
com/google/common/collect/ForwardingDeque.class
|
||||
com/google/common/collect/ForwardingImmutableCollection.class
|
||||
com/google/common/collect/ForwardingImmutableList.class
|
||||
com/google/common/collect/ForwardingImmutableMap.class
|
||||
com/google/common/collect/ForwardingImmutableSet.class
|
||||
com/google/common/collect/ForwardingIterator.class
|
||||
com/google/common/collect/ForwardingListIterator.class
|
||||
com/google/common/collect/ForwardingListMultimap.class
|
||||
com/google/common/collect/ForwardingNavigableMap$StandardDescendingMap$1.class
|
||||
com/google/common/collect/ForwardingNavigableMap$StandardDescendingMap.class
|
||||
com/google/common/collect/ForwardingNavigableMap$StandardNavigableKeySet.class
|
||||
com/google/common/collect/ForwardingNavigableMap.class
|
||||
com/google/common/collect/ForwardingQueue.class
|
||||
com/google/common/collect/ForwardingSetMultimap.class
|
||||
com/google/common/collect/ForwardingSortedMultiset$StandardDescendingMultiset.class
|
||||
com/google/common/collect/ForwardingSortedMultiset$StandardElementSet.class
|
||||
com/google/common/collect/ForwardingSortedMultiset.class
|
||||
com/google/common/collect/ForwardingSortedSetMultimap.class
|
||||
com/google/common/collect/ForwardingTable.class
|
||||
com/google/common/collect/GeneralRange.class
|
||||
com/google/common/collect/GwtTransient.class
|
||||
com/google/common/collect/HashBasedTable$Factory.class
|
||||
com/google/common/collect/HashBasedTable.class
|
||||
com/google/common/collect/HashBiMap$1$MapEntry.class
|
||||
com/google/common/collect/HashBiMap$1.class
|
||||
com/google/common/collect/HashBiMap$BiEntry.class
|
||||
com/google/common/collect/HashBiMap$Inverse$1$InverseEntry.class
|
||||
com/google/common/collect/HashBiMap$Inverse$1.class
|
||||
com/google/common/collect/HashBiMap$Inverse$InverseKeySet$1.class
|
||||
com/google/common/collect/HashBiMap$Inverse$InverseKeySet.class
|
||||
com/google/common/collect/HashBiMap$Inverse.class
|
||||
com/google/common/collect/HashBiMap$InverseSerializedForm.class
|
||||
com/google/common/collect/HashBiMap$Itr.class
|
||||
com/google/common/collect/HashBiMap$KeySet$1.class
|
||||
com/google/common/collect/HashBiMap$KeySet.class
|
||||
com/google/common/collect/HashBiMap.class
|
||||
com/google/common/collect/HashMultimap.class
|
||||
com/google/common/collect/HashMultimapGwtSerializationDependencies.class
|
||||
com/google/common/collect/ImmutableClassToInstanceMap$1.class
|
||||
com/google/common/collect/ImmutableClassToInstanceMap$Builder.class
|
||||
com/google/common/collect/ImmutableClassToInstanceMap.class
|
||||
com/google/common/collect/ImmutableSortedMultiset$Builder.class
|
||||
com/google/common/collect/ImmutableSortedMultiset$SerializedForm.class
|
||||
com/google/common/collect/ImmutableSortedMultiset.class
|
||||
com/google/common/collect/ImmutableSortedMultisetFauxverideShim.class
|
||||
com/google/common/collect/ImmutableTable$1.class
|
||||
com/google/common/collect/ImmutableTable$Builder.class
|
||||
com/google/common/collect/ImmutableTable$CollectorState.class
|
||||
com/google/common/collect/ImmutableTable$MutableCell.class
|
||||
com/google/common/collect/ImmutableTable$SerializedForm.class
|
||||
com/google/common/collect/ImmutableTable.class
|
||||
com/google/common/collect/Interner.class
|
||||
com/google/common/collect/Interners$1.class
|
||||
com/google/common/collect/Interners$InternerBuilder.class
|
||||
com/google/common/collect/Interners$InternerFunction.class
|
||||
com/google/common/collect/Interners$InternerImpl.class
|
||||
com/google/common/collect/Interners.class
|
||||
com/google/common/collect/LinkedHashMultimap$1.class
|
||||
com/google/common/collect/LinkedHashMultimap$ValueEntry.class
|
||||
com/google/common/collect/LinkedHashMultimap$ValueSet$1.class
|
||||
com/google/common/collect/LinkedHashMultimap$ValueSet.class
|
||||
com/google/common/collect/LinkedHashMultimap$ValueSetLink.class
|
||||
com/google/common/collect/LinkedHashMultimap.class
|
||||
com/google/common/collect/LinkedHashMultimapGwtSerializationDependencies.class
|
||||
com/google/common/collect/LinkedListMultimap$1.class
|
||||
com/google/common/collect/LinkedListMultimap$1EntriesImpl.class
|
||||
com/google/common/collect/LinkedListMultimap$1KeySetImpl.class
|
||||
com/google/common/collect/LinkedListMultimap$1ValuesImpl$1.class
|
||||
com/google/common/collect/LinkedListMultimap$1ValuesImpl.class
|
||||
com/google/common/collect/LinkedListMultimap$DistinctKeyIterator.class
|
||||
com/google/common/collect/LinkedListMultimap$KeyList.class
|
||||
com/google/common/collect/LinkedListMultimap$Node.class
|
||||
com/google/common/collect/LinkedListMultimap$NodeIterator.class
|
||||
com/google/common/collect/LinkedListMultimap$ValueForKeyIterator.class
|
||||
com/google/common/collect/LinkedListMultimap.class
|
||||
com/google/common/collect/MinMaxPriorityQueue$1.class
|
||||
com/google/common/collect/MinMaxPriorityQueue$Builder.class
|
||||
com/google/common/collect/MinMaxPriorityQueue$Heap.class
|
||||
com/google/common/collect/MinMaxPriorityQueue$MoveDesc.class
|
||||
com/google/common/collect/MinMaxPriorityQueue$QueueIterator.class
|
||||
com/google/common/collect/MinMaxPriorityQueue.class
|
||||
com/google/common/collect/MoreCollectors$ToOptionalState.class
|
||||
com/google/common/collect/MoreCollectors.class
|
||||
com/google/common/collect/MutableClassToInstanceMap$1.class
|
||||
com/google/common/collect/MutableClassToInstanceMap$2$1.class
|
||||
com/google/common/collect/MutableClassToInstanceMap$2.class
|
||||
com/google/common/collect/MutableClassToInstanceMap$SerializedForm.class
|
||||
com/google/common/collect/MutableClassToInstanceMap.class
|
||||
com/google/common/collect/Queues.class
|
||||
com/google/common/collect/RegularImmutableSortedMultiset.class
|
||||
com/google/common/collect/RegularImmutableTable$1.class
|
||||
com/google/common/collect/RegularImmutableTable$CellSet.class
|
||||
com/google/common/collect/RegularImmutableTable$Values.class
|
||||
com/google/common/collect/RegularImmutableTable.class
|
||||
com/google/common/collect/RowSortedTable.class
|
||||
com/google/common/collect/SingletonImmutableTable.class
|
||||
com/google/common/collect/SortedMultisets$ElementSet.class
|
||||
com/google/common/collect/SortedMultisets$NavigableElementSet.class
|
||||
com/google/common/collect/SortedMultisets.class
|
||||
com/google/common/collect/SparseImmutableTable.class
|
||||
com/google/common/collect/StandardRowSortedTable$1.class
|
||||
com/google/common/collect/StandardRowSortedTable$RowSortedMap.class
|
||||
com/google/common/collect/StandardRowSortedTable.class
|
||||
com/google/common/collect/StandardTable$1.class
|
||||
com/google/common/collect/StandardTable$CellIterator.class
|
||||
com/google/common/collect/StandardTable$Column$EntrySet.class
|
||||
com/google/common/collect/StandardTable$Column$EntrySetIterator$1EntryImpl.class
|
||||
com/google/common/collect/StandardTable$Column$EntrySetIterator.class
|
||||
com/google/common/collect/StandardTable$Column$KeySet.class
|
||||
com/google/common/collect/StandardTable$Column$Values.class
|
||||
com/google/common/collect/StandardTable$Column.class
|
||||
com/google/common/collect/StandardTable$ColumnKeyIterator.class
|
||||
com/google/common/collect/StandardTable$ColumnKeySet.class
|
||||
com/google/common/collect/StandardTable$ColumnMap$ColumnMapEntrySet$1.class
|
||||
com/google/common/collect/StandardTable$ColumnMap$ColumnMapEntrySet.class
|
||||
com/google/common/collect/StandardTable$ColumnMap$ColumnMapValues.class
|
||||
com/google/common/collect/StandardTable$ColumnMap.class
|
||||
com/google/common/collect/StandardTable$Row$1.class
|
||||
com/google/common/collect/StandardTable$Row$2.class
|
||||
com/google/common/collect/StandardTable$Row.class
|
||||
com/google/common/collect/StandardTable$RowMap$EntrySet$1.class
|
||||
com/google/common/collect/StandardTable$RowMap$EntrySet.class
|
||||
com/google/common/collect/StandardTable$RowMap.class
|
||||
com/google/common/collect/StandardTable$TableSet.class
|
||||
com/google/common/collect/StandardTable.class
|
||||
com/google/common/collect/Tables$1.class
|
||||
com/google/common/collect/Tables$AbstractCell.class
|
||||
com/google/common/collect/Tables$ImmutableCell.class
|
||||
com/google/common/collect/Tables$TransformedTable$1.class
|
||||
com/google/common/collect/Tables$TransformedTable$2.class
|
||||
com/google/common/collect/Tables$TransformedTable$3.class
|
||||
com/google/common/collect/Tables$TransformedTable.class
|
||||
com/google/common/collect/Tables$TransposeTable$1.class
|
||||
com/google/common/collect/Tables$TransposeTable.class
|
||||
com/google/common/collect/Tables$UnmodifiableRowSortedMap.class
|
||||
com/google/common/collect/Tables$UnmodifiableTable.class
|
||||
com/google/common/collect/Tables.class
|
||||
com/google/common/collect/TreeBasedTable$1.class
|
||||
com/google/common/collect/TreeBasedTable$2.class
|
||||
com/google/common/collect/TreeBasedTable$Factory.class
|
||||
com/google/common/collect/TreeBasedTable$TreeRow.class
|
||||
com/google/common/collect/TreeBasedTable.class
|
||||
com/google/common/collect/TreeMultimap.class
|
||||
com/google/common/collect/TreeMultiset$1.class
|
||||
com/google/common/collect/TreeMultiset$2.class
|
||||
com/google/common/collect/TreeMultiset$3.class
|
||||
com/google/common/collect/TreeMultiset$4.class
|
||||
com/google/common/collect/TreeMultiset$Aggregate$1.class
|
||||
com/google/common/collect/TreeMultiset$Aggregate$2.class
|
||||
com/google/common/collect/TreeMultiset$Aggregate.class
|
||||
com/google/common/collect/TreeMultiset$AvlNode.class
|
||||
com/google/common/collect/TreeMultiset$Reference.class
|
||||
com/google/common/collect/TreeMultiset.class
|
||||
com/google/common/collect/TreeRangeMap$1.class
|
||||
com/google/common/collect/TreeRangeMap$AsMapOfRanges.class
|
||||
com/google/common/collect/TreeRangeMap$RangeMapEntry.class
|
||||
com/google/common/collect/TreeRangeMap$SubRangeMap$1$1.class
|
||||
com/google/common/collect/TreeRangeMap$SubRangeMap$1.class
|
||||
com/google/common/collect/TreeRangeMap$SubRangeMap$SubRangeMapAsMap$1.class
|
||||
com/google/common/collect/TreeRangeMap$SubRangeMap$SubRangeMapAsMap$2.class
|
||||
com/google/common/collect/TreeRangeMap$SubRangeMap$SubRangeMapAsMap$3.class
|
||||
com/google/common/collect/TreeRangeMap$SubRangeMap$SubRangeMapAsMap$4.class
|
||||
com/google/common/collect/TreeRangeMap$SubRangeMap$SubRangeMapAsMap.class
|
||||
com/google/common/collect/TreeRangeMap$SubRangeMap.class
|
||||
com/google/common/collect/TreeRangeMap.class
|
||||
com/google/common/collect/TreeTraverser$1.class
|
||||
com/google/common/collect/TreeTraverser$2$1.class
|
||||
com/google/common/collect/TreeTraverser$2.class
|
||||
com/google/common/collect/TreeTraverser$3$1.class
|
||||
com/google/common/collect/TreeTraverser$3.class
|
||||
com/google/common/collect/TreeTraverser$4.class
|
||||
com/google/common/collect/TreeTraverser$BreadthFirstIterator.class
|
||||
com/google/common/collect/TreeTraverser$PostOrderIterator.class
|
||||
com/google/common/collect/TreeTraverser$PostOrderNode.class
|
||||
com/google/common/collect/TreeTraverser$PreOrderIterator.class
|
||||
com/google/common/collect/TreeTraverser.class
|
||||
@@ -0,0 +1,2 @@
|
||||
ch/qos/logback/core/db/**
|
||||
ch/qos/logback/classic/db/**
|
||||
@@ -0,0 +1,5 @@
|
||||
org/checkerframework/**
|
||||
org/intellij/**
|
||||
com/google/errorprone/**
|
||||
com/google/thirdparty/**
|
||||
com/google/j2objc/**
|
||||
@@ -0,0 +1,14 @@
|
||||
import be.simplenotes.micronaut
|
||||
|
||||
plugins {
|
||||
id("be.simplenotes.base")
|
||||
id("be.simplenotes.micronaut")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
micronaut()
|
||||
runtimeOnly(libs.yaml)
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
testRuntimeOnly(libs.slf4j.logback)
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
jwt:
|
||||
secret: 'PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms='
|
||||
validity: 24
|
||||
time-unit: hours
|
||||
|
||||
server:
|
||||
host: localhost
|
||||
port: 8080
|
||||
|
||||
data-dir: ./data
|
||||
@@ -0,0 +1,57 @@
|
||||
package be.simplenotes.config
|
||||
|
||||
import io.micronaut.context.annotation.*
|
||||
import java.nio.file.Path
|
||||
import java.util.concurrent.TimeUnit
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
data class DataConfig(val dataDir: String)
|
||||
|
||||
data class DataSourceConfig(
|
||||
val jdbcUrl: String,
|
||||
val maximumPoolSize: Int,
|
||||
val connectionTimeout: Long,
|
||||
)
|
||||
|
||||
@ConfigurationProperties("jwt")
|
||||
data class JwtConfig @ConfigurationInject constructor(
|
||||
val secret: String,
|
||||
val validity: Long,
|
||||
val timeUnit: TimeUnit,
|
||||
) {
|
||||
override fun toString() = "JwtConfig(secret='***', validity=$validity, timeUnit=$timeUnit)"
|
||||
}
|
||||
|
||||
@ConfigurationProperties("server")
|
||||
data class ServerConfig @ConfigurationInject constructor(
|
||||
val host: String,
|
||||
val port: Int,
|
||||
)
|
||||
|
||||
@Factory
|
||||
class ConfigFactory {
|
||||
|
||||
@Singleton
|
||||
@Requires(notEnv = ["test"])
|
||||
fun datasourceConfig(dataConfig: DataConfig) = DataSourceConfig(
|
||||
jdbcUrl = "jdbc:h2:" + Path.of(dataConfig.dataDir, "simplenotes").normalize().toAbsolutePath(),
|
||||
maximumPoolSize = 10,
|
||||
connectionTimeout = 1000,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Requires(env = ["test"])
|
||||
fun testDatasourceConfig() = DataSourceConfig(
|
||||
jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1",
|
||||
maximumPoolSize = 2,
|
||||
connectionTimeout = 1000,
|
||||
)
|
||||
|
||||
@Singleton
|
||||
@Requires(notEnv = ["test"])
|
||||
fun dataConfig(@Property(name = "data-dir") dataDir: String) = DataConfig(dataDir)
|
||||
|
||||
@Singleton
|
||||
@Requires(env = ["test"])
|
||||
fun testDataConfig() = DataConfig("/tmp")
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package be.simplenotes.config
|
||||
|
||||
import io.micronaut.context.ApplicationContext
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Test
|
||||
|
||||
class ConfigTest {
|
||||
private val ctx = ApplicationContext.run()
|
||||
|
||||
@Test
|
||||
fun `check application yaml is on the classpath`() {
|
||||
val yaml = javaClass.getResource("/application.yaml")
|
||||
assertThat(yaml).`as`("The application.yaml resource").isNotNull
|
||||
assertThat(yaml.readText()).`as`("The config content").isNotBlank
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `check config properties`() {
|
||||
assertThat(ctx.getProperty("jwt.validity", Int::class.java))
|
||||
.`as`("Jwt.validity")
|
||||
.isPresent
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `load jwt config`() {
|
||||
val jwtConfig = ctx.getBean(JwtConfig::class.java)
|
||||
assertThat(jwtConfig).isNotNull
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<configuration>
|
||||
<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
|
||||
<encoder>
|
||||
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
|
||||
</pattern>
|
||||
</encoder>
|
||||
</appender>
|
||||
<root level="DEBUG">
|
||||
<appender-ref ref="STDOUT"/>
|
||||
</root>
|
||||
<logger name="io.micronaut" level="TRACE"/>
|
||||
<logger name="io.micronaut.context.lifecycle" level="TRACE"/>
|
||||
</configuration>
|
||||
@@ -0,0 +1,4 @@
|
||||
plugins {
|
||||
id("be.simplenotes.java-convention")
|
||||
id("be.simplenotes.postcss")
|
||||
}
|
||||
@@ -2,16 +2,14 @@
|
||||
"name": "css",
|
||||
"version": "1.0.0",
|
||||
"scripts": {
|
||||
"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=../simplenotes-app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../simplenotes-app/src/main/resources/static/styles.css"
|
||||
"//": "`gradle css`"
|
||||
},
|
||||
"dependencies": {
|
||||
"autoprefixer": "^9.8.6",
|
||||
"cssnano": "^4.1.10",
|
||||
"postcss-cli": "^7.1.1",
|
||||
"postcss-hash": "^2.0.0",
|
||||
"postcss-import": "^12.0.1",
|
||||
"postcss-nested": "^4.2.3",
|
||||
"tailwindcss": "^1.5.1"
|
||||
"autoprefixer": "^10.4.14",
|
||||
"cssnano": "^6.0.1",
|
||||
"postcss-cli": "^10.1.0",
|
||||
"postcss-hash": "^3.0.0",
|
||||
"postcss-import": "^15.1.0",
|
||||
"tailwindcss": "^3.3.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
module.exports = {
|
||||
plugins: [
|
||||
require('postcss-import'),
|
||||
require('postcss-nested'),
|
||||
require('tailwindcss/nesting'),
|
||||
require('tailwindcss'),
|
||||
require('autoprefixer'),
|
||||
require('cssnano')({
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@apply font-semibold py-2 px-4 rounded;
|
||||
|
||||
&:focus {
|
||||
@apply outline-none shadow-outline;
|
||||
@apply outline-none ring;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
#note {
|
||||
a {
|
||||
@apply text-blue-700 underline;
|
||||
@apply text-blue-500 underline;
|
||||
}
|
||||
|
||||
p {
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
@apply italic font-semibold text-sm bg-teal-500 text-gray-900 rounded-full py-1 px-2 align-middle;
|
||||
|
||||
&:focus {
|
||||
@apply outline-none shadow-outline bg-teal-800 text-white;
|
||||
@apply outline-none ring bg-teal-800 text-white;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
module.exports = {
|
||||
purge: {
|
||||
content: [
|
||||
'../simplenotes-app/src/main/kotlin/be/simplenotes/app/views/**/*.kt'
|
||||
]
|
||||
},
|
||||
process.env.PURGE
|
||||
],
|
||||
theme: {
|
||||
fontFamily: {
|
||||
'sans': [
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
rm simplenotes-app/src/main/resources/css-manifest.json
|
||||
rm simplenotes-app/src/main/resources/static/styles*
|
||||
|
||||
yarn --cwd css run css-purge \
|
||||
&& docker build -t hubv/simplenotes:latest . \
|
||||
&& docker push hubv/simplenotes:latest
|
||||
@@ -1,56 +1,19 @@
|
||||
version: '2.2'
|
||||
|
||||
services:
|
||||
|
||||
db:
|
||||
image: mariadb:10.5.5
|
||||
container_name: simplenotes-mariadb
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Brussels
|
||||
- MYSQL_DATABASE=simplenotes
|
||||
- MYSQL_USER=simplenotes
|
||||
# .env:
|
||||
# - MYSQL_ROOT_PASSWORD
|
||||
# - MYSQL_PASSWORD
|
||||
volumes:
|
||||
- notes-db-volume:/var/lib/mysql
|
||||
healthcheck:
|
||||
test: "mysql --protocol=tcp -u simplenotes -p$MYSQL_PASSWORD -e 'show databases'"
|
||||
interval: 5s
|
||||
timeout: 1s
|
||||
start_period: 2s
|
||||
retries: 10
|
||||
|
||||
simplenotes:
|
||||
image: hubv/simplenotes
|
||||
container_name: simplenotes
|
||||
env_file:
|
||||
- .env
|
||||
environment:
|
||||
- PUID=1000
|
||||
- PGID=1000
|
||||
- TZ=Europe/Brussels
|
||||
- HOST=0.0.0.0
|
||||
- JDBCURL=jdbc:mariadb://db:3306/simplenotes
|
||||
- DRIVERCLASSNAME=org.mariadb.jdbc.Driver
|
||||
- USERNAME=simplenotes
|
||||
# .env:
|
||||
# - JWT_SECRET
|
||||
# - PASSWORD
|
||||
ports:
|
||||
- 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:
|
||||
db:
|
||||
condition: service_healthy
|
||||
|
||||
|
||||
volumes:
|
||||
notes-db-volume:
|
||||
- ./simplenotes-data:/app/data
|
||||
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import be.simplenotes.micronaut
|
||||
|
||||
plugins {
|
||||
id("be.simplenotes.base")
|
||||
id("be.simplenotes.kotlinx-serialization")
|
||||
id("be.simplenotes.micronaut")
|
||||
}
|
||||
|
||||
dependencies {
|
||||
api(project(":config"))
|
||||
api(project(":types"))
|
||||
implementation(project(":persistence"))
|
||||
implementation(project(":search"))
|
||||
|
||||
api(libs.arrow.core)
|
||||
api(libs.konform)
|
||||
|
||||
micronaut()
|
||||
|
||||
implementation(libs.kotlinx.serialization.json)
|
||||
implementation(libs.bcrypt)
|
||||
implementation(libs.jwt)
|
||||
implementation(libs.bundles.flexmark)
|
||||
implementation(libs.yaml)
|
||||
implementation(libs.owasp.html.sanitizer)
|
||||
implementation(libs.commonsCompress)
|
||||
|
||||
testImplementation(libs.bundles.test)
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
package be.simplenotes.domain.usecases.export
|
||||
package be.simplenotes.domain
|
||||
|
||||
import be.simplenotes.persistence.repositories.NoteRepository
|
||||
import be.simplenotes.types.ExportedNote
|
||||
import be.simplenotes.persistance.repositories.NoteRepository
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
|
||||
@@ -9,8 +9,19 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.InputStream
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
interface ExportService {
|
||||
fun exportAsJson(userId: Int): String
|
||||
fun exportAsZip(userId: Int): InputStream
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class ExportServiceImpl(
|
||||
private val noteRepository: NoteRepository,
|
||||
private val json: Json,
|
||||
) : ExportService {
|
||||
|
||||
internal class ExportUseCaseImpl(private val noteRepository: NoteRepository, private val json: Json) : ExportUseCase {
|
||||
override fun exportAsJson(userId: Int): String {
|
||||
val notes = noteRepository.export(userId)
|
||||
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)
|
||||
@@ -31,8 +42,7 @@ internal class ExportUseCaseImpl(private val noteRepository: NoteRepository, pri
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class ZipOutput : AutoCloseable {
|
||||
private class ZipOutput : AutoCloseable {
|
||||
val outputStream = ByteArrayOutputStream()
|
||||
private val zipOutputStream = ZipArchiveOutputStream(outputStream)
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package be.simplenotes.domain
|
||||
|
||||
import be.simplenotes.domain.security.HtmlSanitizer
|
||||
import be.simplenotes.persistence.repositories.NoteRepository
|
||||
import be.simplenotes.types.ExportedNote
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import jakarta.inject.Singleton
|
||||
import kotlinx.serialization.builtins.ListSerializer
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
interface ImportService {
|
||||
fun importJson(user: LoggedInUser, content: String)
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class ImportServiceImpl(
|
||||
private val noteRepository: NoteRepository,
|
||||
private val json: Json,
|
||||
private val htmlSanitizer: HtmlSanitizer,
|
||||
) : ImportService {
|
||||
override fun importJson(user: LoggedInUser, content: String) {
|
||||
val notes = json.decodeFromString(ListSerializer(ExportedNote.serializer()), content)
|
||||
noteRepository.import(
|
||||
user.userId,
|
||||
notes.map {
|
||||
it.copy(html = htmlSanitizer.sanitize(user, it.html))
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package be.simplenotes.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import be.simplenotes.domain.validation.NoteValidations
|
||||
import be.simplenotes.types.NoteMetadata
|
||||
import com.vladsch.flexmark.html.HtmlRenderer
|
||||
import com.vladsch.flexmark.parser.Parser
|
||||
import io.konform.validation.ValidationErrors
|
||||
import jakarta.inject.Singleton
|
||||
import org.yaml.snakeyaml.Yaml
|
||||
import org.yaml.snakeyaml.parser.ParserException
|
||||
import org.yaml.snakeyaml.scanner.ScannerException
|
||||
|
||||
interface MarkdownService {
|
||||
fun renderDocument(input: String): Either<MarkdownParsingError, Document>
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class MarkdownServiceImpl(
|
||||
private val parser: Parser,
|
||||
private val renderer: HtmlRenderer,
|
||||
) : MarkdownService {
|
||||
private val yamlBoundPattern = "-{3}".toRegex()
|
||||
private fun splitMetaFromDocument(input: String) = either {
|
||||
val split = input.split(yamlBoundPattern, 3)
|
||||
ensure(split.size >= 3) { MarkdownParsingError.MissingMeta }
|
||||
split[1].trim() to split[2].trim()
|
||||
}
|
||||
|
||||
private val yaml = Yaml()
|
||||
private fun parseMeta(input: String) = either {
|
||||
val load: Map<String, Any> = try {
|
||||
yaml.load(input)
|
||||
} catch (e: ParserException) {
|
||||
raise(MarkdownParsingError.InvalidMeta)
|
||||
} catch (e: ScannerException) {
|
||||
raise(MarkdownParsingError.InvalidMeta)
|
||||
}
|
||||
|
||||
val title = when (val titleNode = load["title"]) {
|
||||
is String, is Number -> titleNode.toString()
|
||||
else -> raise(MarkdownParsingError.InvalidMeta)
|
||||
}
|
||||
|
||||
val tagsNode = load["tags"]
|
||||
val tags = if (tagsNode !is List<*>)
|
||||
emptyList()
|
||||
else
|
||||
tagsNode.map { it.toString() }
|
||||
|
||||
NoteMetadata(title, tags)
|
||||
}
|
||||
|
||||
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
|
||||
|
||||
override fun renderDocument(input: String) = either {
|
||||
val (meta, md) = splitMetaFromDocument(input).bind()
|
||||
val parsedMeta = parseMeta(meta).bind()
|
||||
NoteValidations.validateMetadata(parsedMeta)?.let { raise(it) }
|
||||
val html = renderMarkdown(md)
|
||||
Document(parsedMeta, html)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class MarkdownParsingError {
|
||||
object MissingMeta : MarkdownParsingError()
|
||||
object InvalidMeta : MarkdownParsingError()
|
||||
class ValidationError(val validationErrors: ValidationErrors) : MarkdownParsingError()
|
||||
}
|
||||
|
||||
data class Document(val metadata: NoteMetadata, val html: String)
|
||||
@@ -1,44 +1,52 @@
|
||||
package be.simplenotes.domain.usecases
|
||||
package be.simplenotes.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.extensions.fx
|
||||
import be.simplenotes.types.Note
|
||||
import be.simplenotes.types.PersistedNote
|
||||
import be.simplenotes.types.PersistedNoteMetadata
|
||||
import arrow.core.raise.either
|
||||
import be.simplenotes.domain.security.HtmlSanitizer
|
||||
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
|
||||
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
|
||||
import be.simplenotes.persistance.repositories.NoteRepository
|
||||
import be.simplenotes.persistance.repositories.UserRepository
|
||||
import be.simplenotes.domain.utils.parseSearchTerms
|
||||
import be.simplenotes.persistence.repositories.NoteRepository
|
||||
import be.simplenotes.persistence.repositories.UserRepository
|
||||
import be.simplenotes.search.NoteSearcher
|
||||
import be.simplenotes.search.SearchTerms
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.types.Note
|
||||
import be.simplenotes.types.PersistedNoteMetadata
|
||||
import jakarta.annotation.PostConstruct
|
||||
import jakarta.annotation.PreDestroy
|
||||
import jakarta.inject.Singleton
|
||||
import java.util.*
|
||||
|
||||
@Singleton
|
||||
class NoteService(
|
||||
private val markdownConverter: MarkdownConverter,
|
||||
private val markdownService: MarkdownService,
|
||||
private val noteRepository: NoteRepository,
|
||||
private val userRepository: UserRepository,
|
||||
private val searcher: NoteSearcher,
|
||||
private val htmlSanitizer: HtmlSanitizer,
|
||||
) {
|
||||
|
||||
fun create(userId: Int, markdownText: String) = Either.fx<MarkdownParsingError, PersistedNote> {
|
||||
val persistedNote = !markdownConverter.renderDocument(markdownText)
|
||||
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
|
||||
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||
.map { noteRepository.create(userId, it) }
|
||||
|
||||
searcher.indexNote(userId, persistedNote)
|
||||
persistedNote
|
||||
fun create(user: LoggedInUser, markdownText: String) = either {
|
||||
markdownService.renderDocument(markdownText)
|
||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||
.map { Note(title = it.metadata.title, tags = it.metadata.tags, markdown = markdownText, html = it.html) }
|
||||
.map { noteRepository.create(user.userId, it) }
|
||||
.bind()
|
||||
.also { searcher.indexNote(user.userId, it) }
|
||||
}
|
||||
|
||||
fun update(userId: Int, uuid: UUID, markdownText: String) = Either.fx<MarkdownParsingError, PersistedNote?> {
|
||||
val persistedNote = !markdownConverter.renderDocument(markdownText)
|
||||
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
|
||||
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
|
||||
.map { noteRepository.update(userId, uuid, it) }
|
||||
|
||||
persistedNote?.let { searcher.updateIndex(userId, it) }
|
||||
persistedNote
|
||||
fun update(user: LoggedInUser, uuid: UUID, markdownText: String) =
|
||||
either {
|
||||
markdownService.renderDocument(markdownText)
|
||||
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
|
||||
.map {
|
||||
Note(
|
||||
title = it.metadata.title,
|
||||
tags = it.metadata.tags,
|
||||
markdown = markdownText,
|
||||
html = it.html,
|
||||
)
|
||||
}
|
||||
.map { noteRepository.update(user.userId, uuid, it) }
|
||||
.bind()
|
||||
?.also { searcher.updateIndex(user.userId, it) }
|
||||
}
|
||||
|
||||
fun paginatedNotes(
|
||||
@@ -77,7 +85,9 @@ class NoteService(
|
||||
|
||||
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
|
||||
|
||||
@PostConstruct
|
||||
fun indexAll() {
|
||||
dropAllIndexes()
|
||||
val userIds = userRepository.findAll()
|
||||
userIds.forEach { id ->
|
||||
val notes = noteRepository.findAllDetails(id)
|
||||
@@ -85,8 +95,9 @@ class NoteService(
|
||||
}
|
||||
}
|
||||
|
||||
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
|
||||
fun search(userId: Int, searchInput: String) = searcher.search(userId, parseSearchTerms(searchInput))
|
||||
|
||||
@PreDestroy
|
||||
fun dropAllIndexes() = searcher.dropAll()
|
||||
|
||||
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
|
||||
@@ -0,0 +1,80 @@
|
||||
package be.simplenotes.domain
|
||||
|
||||
import arrow.core.Either
|
||||
import arrow.core.raise.either
|
||||
import arrow.core.raise.ensure
|
||||
import arrow.core.raise.ensureNotNull
|
||||
import be.simplenotes.domain.security.PasswordHash
|
||||
import be.simplenotes.domain.security.SimpleJwt
|
||||
import be.simplenotes.domain.validation.UserValidations
|
||||
import be.simplenotes.persistence.repositories.UserRepository
|
||||
import be.simplenotes.search.NoteSearcher
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import be.simplenotes.types.PersistedUser
|
||||
import io.konform.validation.ValidationErrors
|
||||
import jakarta.inject.Singleton
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
interface UserService {
|
||||
fun register(form: RegisterForm): Either<RegisterError, PersistedUser>
|
||||
fun login(form: LoginForm): Either<LoginError, Token>
|
||||
fun delete(form: DeleteForm): Either<DeleteError, Unit>
|
||||
}
|
||||
|
||||
@Singleton
|
||||
internal class UserServiceImpl(
|
||||
private val userRepository: UserRepository,
|
||||
private val passwordHash: PasswordHash,
|
||||
private val jwt: SimpleJwt<LoggedInUser>,
|
||||
private val searcher: NoteSearcher,
|
||||
) : UserService {
|
||||
|
||||
override fun register(form: RegisterForm) = either {
|
||||
val user = UserValidations.validateRegister(form).bind()
|
||||
ensure(!userRepository.exists(user.username)) { RegisterError.UserExists }
|
||||
ensureNotNull(userRepository.create(user.copy(password = passwordHash.crypt(user.password)))) {
|
||||
RegisterError.UserExists
|
||||
}
|
||||
}
|
||||
|
||||
override fun login(form: LoginForm) = either {
|
||||
val user = UserValidations.validateLogin(form).bind()
|
||||
val persistedUser = ensureNotNull(userRepository.find(user.username)) { LoginError.Unregistered }
|
||||
ensure(passwordHash.verify(form.password!!, persistedUser.password)) { LoginError.WrongPassword }
|
||||
jwt.sign(LoggedInUser(persistedUser))
|
||||
}
|
||||
|
||||
override fun delete(form: DeleteForm) = either {
|
||||
val user = UserValidations.validateDelete(form).bind()
|
||||
val persistedUser = ensureNotNull(userRepository.find(user.username)) { DeleteError.Unregistered }
|
||||
ensure(passwordHash.verify(user.password, persistedUser.password)) { DeleteError.WrongPassword }
|
||||
ensure(userRepository.delete(persistedUser.id)) { DeleteError.Unregistered }
|
||||
searcher.dropIndex(persistedUser.id)
|
||||
}
|
||||
}
|
||||
|
||||
sealed class DeleteError {
|
||||
object Unregistered : DeleteError()
|
||||
object WrongPassword : DeleteError()
|
||||
class InvalidForm(val validationErrors: ValidationErrors) : DeleteError()
|
||||
}
|
||||
|
||||
data class DeleteForm(val username: String?, val password: String?, val checked: Boolean)
|
||||
|
||||
sealed class LoginError {
|
||||
object Unregistered : LoginError()
|
||||
object WrongPassword : LoginError()
|
||||
class InvalidLoginForm(val validationErrors: ValidationErrors) : LoginError()
|
||||
}
|
||||
|
||||
typealias Token = String
|
||||
|
||||
sealed class RegisterError {
|
||||
object UserExists : RegisterError()
|
||||
class InvalidRegisterForm(val validationErrors: ValidationErrors) : RegisterError()
|
||||
}
|
||||
|
||||
typealias RegisterForm = LoginForm
|
||||
|
||||
@Serializable
|
||||
data class LoginForm(val username: String?, val password: String?)
|
||||
@@ -0,0 +1,34 @@
|
||||
package be.simplenotes.domain.modules
|
||||
|
||||
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
|
||||
import com.vladsch.flexmark.html.HtmlRenderer
|
||||
import com.vladsch.flexmark.parser.Parser
|
||||
import com.vladsch.flexmark.util.data.MutableDataSet
|
||||
import io.micronaut.context.annotation.Factory
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Factory
|
||||
class FlexmarkFactory {
|
||||
|
||||
@Singleton
|
||||
fun parser(options: MutableDataSet) = Parser.builder(options).build()
|
||||
|
||||
@Singleton
|
||||
fun htmlRenderer(options: MutableDataSet) = HtmlRenderer.builder(options).build()
|
||||
|
||||
@Singleton
|
||||
fun options() = MutableDataSet().apply {
|
||||
set(Parser.EXTENSIONS, listOf(TaskListExtension.create()))
|
||||
set(TaskListExtension.TIGHT_ITEM_CLASS, "")
|
||||
set(TaskListExtension.LOOSE_ITEM_CLASS, "")
|
||||
set(
|
||||
TaskListExtension.ITEM_DONE_MARKER,
|
||||
"""<input type="checkbox" checked="checked" disabled="disabled" readonly="readonly" /> """,
|
||||
)
|
||||
set(
|
||||
TaskListExtension.ITEM_NOT_DONE_MARKER,
|
||||
"""<input type="checkbox" disabled="disabled" readonly="readonly" /> """,
|
||||
)
|
||||
set(HtmlRenderer.SOFT_BREAK, "<br>")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package be.simplenotes.domain.security
|
||||
|
||||
import be.simplenotes.types.LoggedInUser
|
||||
import org.owasp.html.HtmlChangeListener
|
||||
import org.owasp.html.HtmlPolicyBuilder
|
||||
import org.slf4j.LoggerFactory
|
||||
import jakarta.inject.Singleton
|
||||
|
||||
@Singleton
|
||||
class HtmlSanitizer {
|
||||
private val htmlPolicy = HtmlPolicyBuilder()
|
||||
.allowElements("a")
|
||||
.allowCommonBlockElements()
|
||||
.allowCommonInlineFormattingElements()
|
||||
.allowElements("pre")
|
||||
.allowAttributes("class").onElements("code")
|
||||
.allowUrlProtocols("http", "https")
|
||||
.allowAttributes("href").onElements("a")
|
||||
.allowElements("input")
|
||||
.allowAttributes("type", "checked", "disabled", "readonly").onElements("input")
|
||||
.requireRelNofollowOnLinks()
|
||||
.toFactory()!!
|
||||
|
||||
private val logger = LoggerFactory.getLogger(javaClass)
|
||||
|
||||
private val htmlChangeListener = object : HtmlChangeListener<LoggedInUser> {
|
||||
override fun discardedTag(context: LoggedInUser?, elementName: String) {
|
||||
logger.warn("Discarded tag $elementName for user $context")
|
||||
}
|
||||
|
||||
override fun discardedAttributes(context: LoggedInUser?, tagName: String, vararg attributeNames: String) {
|
||||
logger.warn("Discarded attributes ${attributeNames.contentToString()} on tag $tagName for user $context")
|
||||
}
|
||||
}
|
||||
|
||||
fun sanitize(userId: LoggedInUser, unsafeHtml: String) =
|
||||
htmlPolicy.sanitize(unsafeHtml, htmlChangeListener, userId)!!
|
||||
}
|
||||