21 Commits

Author SHA1 Message Date
hubert 4f395d254d Merge branch 'micronaut' into master 2020-11-02 00:34:31 +01:00
hubert 6a43acfd46 Switch from koin to micronaut-inject 2020-11-02 00:33:57 +01:00
hubert 78b84dc62a Add more persistance tests 2020-11-02 00:33:57 +01:00
hubert 1120bc9350 Add .sdkmanrc 2020-10-29 16:57:39 +01:00
hubert a37254452b Fix webmanifest 2020-10-28 02:58:30 +01:00
hubert cd9fdd28e8 Gradle stuff 2020-10-28 02:01:16 +01:00
hubert c3fc6a4e88 Clean gradle scripts 2020-10-28 00:42:51 +01:00
hubert cb58a4fbe0 Fix lucene illegal reflective access warning 2020-10-26 22:35:27 +01:00
hubert 64059984d3 Nice 2020-10-26 22:24:56 +01:00
hubert fdc8d34f82 Move postcss purge config to gradle 2020-10-26 22:19:15 +01:00
hubert 95ec674eb8 Fix ignored gradle wrapper jar 2020-10-26 21:18:02 +01:00
hubert ea7be84ec3 Extract serialization plugin 2020-10-26 21:17:19 +01:00
hubert c709f2b44d Add ktlint plugin 2020-10-26 21:17:19 +01:00
hubert 7995a0b3e0 Change arrow core -> arrow core data 2020-10-26 21:15:59 +01:00
hubert bfd562bc60 Add Gradle Wrapper 2020-10-26 02:10:22 +01:00
hubert 4fb85a52e4 Use Gradle ! 2020-10-26 02:10:22 +01:00
hubert e64352f54c Fix arrow depreciations 2020-10-25 23:46:56 +01:00
hubert c2c03e415e Move Logback.xml 2020-10-24 01:36:51 +02:00
hubert 0260bea951 Move ConfigLoader 2020-10-24 01:36:51 +02:00
hubert 8b8dbd6fe5 Separate views into a maven module 2020-10-24 01:36:51 +02:00
hubert 536c6e7b79 Merge branch 'jigsaw' into master 2020-10-24 00:05:09 +02:00
141 changed files with 2412 additions and 1824 deletions
+5 -97
View File
@@ -1,24 +1,6 @@
# Java # Gradle
.mtj.tmp/ build/
*.class .gradle
*.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
# IntelliJ # IntelliJ
out/ out/
@@ -28,11 +10,8 @@ out/
*.ipr *.ipr
*.iws *.iws
# Vue
node_modules
/dist
# Local env files # Local env files
.env
.env.local .env.local
.env.*.local .env.*.local
@@ -49,85 +28,13 @@ pids
*.seed *.seed
*.pid.lock *.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 # Dependency directories
node_modules/ node_modules/
jspm_packages/ 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 file
.yarn-integrity .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 # h2 db
*.db *.db
@@ -136,3 +43,4 @@ simplenotes-app/src/main/resources/static/styles*
# python # python
__pycache__ __pycache__
+5
View File
@@ -0,0 +1,5 @@
# Enable auto-env through the sdkman_auto_env config
# Add key=value pairs of SDKs to use below
java=14.0.2-open
gradle=6.7
kotlin=1.4.10
+15 -28
View File
@@ -1,29 +1,3 @@
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 openjdk:14-alpine as jdkbuilder
RUN apk add --no-cache binutils RUN apk add --no-cache binutils
@@ -46,8 +20,21 @@ RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER USER $APPLICATION_USER
COPY --from=builder /tmp/simplenotes-app/target/simplenotes-app-*.jar /app/simplenotes.jar
COPY --from=jdkbuilder /myjdk /myjdk COPY --from=jdkbuilder /myjdk /myjdk
COPY simplenotes-app/build/libs/simplenotes-app-with-dependencies*.jar /app/simplenotes.jar
WORKDIR /app WORKDIR /app
CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms64m", "-Xmx256m", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "simplenotes.jar"] 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" \
]
+2
View File
@@ -0,0 +1,2 @@
org.gradle.caching=true
org.gradle.parallel=true
+20
View File
@@ -0,0 +1,20 @@
plugins {
`kotlin-dsl`
}
kotlinDslPluginOptions {
experimentalWarning.set(false)
}
repositories {
gradlePluginPortal()
maven { setUrl("https://kotlin.bintray.com/kotlinx") }
}
dependencies {
implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.4.10")
implementation("org.jetbrains.kotlin:kotlin-serialization:1.4.10")
implementation("com.github.jengelman.gradle.plugins:shadow:6.1.0")
implementation("org.jlleitschuh.gradle:ktlint-gradle:9.4.1")
implementation("com.github.ben-manes:gradle-versions-plugin:0.28.0")
}
@@ -0,0 +1,77 @@
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 CssTask : DefaultTask() {
private val root = project.parent!!.rootDir
private val viewsProject = project
.parent
?.project(":simplenotes-views")
?: error("Missing :simplenotes-views")
@get:InputDirectory
val templatesDir = viewsProject.extensions
.getByType<SourceSetContainer>()
.asMap.getOrElse("main") { error("main sources not found") }
.allSource.srcDirs
.find { it.endsWith("kotlin") }
?: 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", "build", "$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,45 @@
@file:Suppress("SpellCheckingInspection")
package be.simplenotes
object Libs {
const val arrowCoreData = "io.arrow-kt:arrow-core-data:0.11.0"
const val commonsCompress = "org.apache.commons:commons-compress:1.20"
const val flexmark = "com.vladsch.flexmark:flexmark:0.62.2"
const val flexmarkGfmTasklist = "com.vladsch.flexmark:flexmark-ext-gfm-tasklist:0.62.2"
const val flywayCore = "org.flywaydb:flyway-core:6.5.4"
const val h2 = "com.h2database:h2:1.4.200"
const val hikariCP = "com.zaxxer:HikariCP:3.4.3"
const val http4kCore = "org.http4k:http4k-core:3.268.0"
const val javaJwt = "com.auth0:java-jwt:3.10.3"
const val javaxServlet = "javax.servlet:javax.servlet-api:4.0.1"
const val jbcrypt = "org.mindrot:jbcrypt:0.4"
const val jettyServer = "org.eclipse.jetty:jetty-server:9.4.32.v20200930"
const val jettyServlet = "org.eclipse.jetty:jetty-servlet:9.4.32.v20200930"
const val konform = "io.konform:konform-jvm:0.2.0"
const val kotlinxHtml = "org.jetbrains.kotlinx:kotlinx-html-jvm:0.7.1"
const val kotlinxSerializationJson = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.0.0"
const val ktormCore = "me.liuwj.ktorm:ktorm-core:3.0.0"
const val ktormMysql = "me.liuwj.ktorm:ktorm-support-mysql:3.0.0"
const val logbackClassic = "ch.qos.logback:logback-classic:1.2.3"
const val luceneAnalyzersCommon = "org.apache.lucene:lucene-analyzers-common:8.6.1"
const val luceneCore = "org.apache.lucene:lucene-core:8.6.1"
const val luceneQueryParser = "org.apache.lucene:lucene-queryparser:8.6.1"
const val mapstruct = "org.mapstruct:mapstruct:1.4.1.Final"
const val mapstructProcessor = "org.mapstruct:mapstruct-processor:1.4.1.Final"
const val micronaut = "io.micronaut:micronaut-inject:2.1.2"
const val micronautProcessor = "io.micronaut:micronaut-inject-java:2.1.2"
const val mariadbClient = "org.mariadb.jdbc:mariadb-java-client:2.6.2"
const val owaspHtmlSanitizer = "com.googlecode.owasp-java-html-sanitizer:owasp-java-html-sanitizer:20200713.1"
const val prettytime ="org.ocpsoft.prettytime:prettytime:4.0.5.Final"
const val slf4jApi = "org.slf4j:slf4j-api:1.7.25"
const val snakeyaml = "org.yaml:snakeyaml:1.26"
const val assertJ = "org.assertj:assertj-core:3.16.1"
const val hamkrest = "com.natpryce:hamkrest:1.7.0.3"
const val http4kTestingHamkrest = "org.http4k:http4k-testing-hamkrest:3.268.0"
const val junit = "org.junit.jupiter:junit-jupiter:5.6.2"
const val mockk = "io.mockk:mockk:1.10.0"
const val faker = "com.github.javafaker:javafaker:1.0.2"
const val mariaTestContainer = "org.testcontainers:mariadb:1.15.0-rc2"
}
@@ -0,0 +1,15 @@
package be.simplenotes
import org.gradle.kotlin.dsl.register
plugins {
java apply false
}
tasks.register<CssTask>("css")
sourceSets {
val main by getting
val root = file("$buildDir/generated-resources/css")
main.resources.srcDir(root)
}
@@ -0,0 +1,23 @@
package be.simplenotes
tasks.create("dockerBuild") {
dependsOn("package")
doLast {
exec {
commandLine("docker", "build", "-t", "hubv/simplenotes:latest", ".")
workingDir(rootProject.projectDir)
}
}
}
tasks.create("dockerPush") {
dependsOn("dockerBuild")
doLast {
exec {
commandLine("docker", "push", "hubv/simplenotes:latest")
workingDir(rootProject.projectDir)
}
}
}
@@ -0,0 +1,38 @@
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"
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")
dependsOn("css")
doLast {
println("SimpleNotes Packaged !")
}
}
@@ -0,0 +1,9 @@
package be.simplenotes
plugins {
id("be.simplenotes.java-convention")
id("be.simplenotes.kotlin-convention")
id("be.simplenotes.junit-convention")
id("org.jlleitschuh.gradle.ktlint")
id("com.github.ben-manes.versions")
}
@@ -0,0 +1,26 @@
package be.simplenotes
plugins {
java
`java-library`
}
repositories {
mavenLocal()
mavenCentral()
jcenter()
maven { url = uri("https://dl.bintray.com/arrow-kt/arrow-kt/") }
maven { url = uri("https://kotlin.bintray.com/kotlinx") }
}
group = "be.simplenotes"
version = "1.0-SNAPSHOT"
java {
sourceCompatibility = JavaVersion.VERSION_14
targetCompatibility = JavaVersion.VERSION_14
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
}
@@ -0,0 +1,14 @@
package be.simplenotes
plugins {
java apply false
}
tasks.withType<Test> {
useJUnitPlatform()
}
sourceSets {
val test by getting
test.resources.srcDir("${rootProject.projectDir}/simplenotes-test-resources/src/test/resources")
}
@@ -0,0 +1,25 @@
package be.simplenotes
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins {
kotlin("jvm")
}
dependencies {
implementation(kotlin("stdlib-jdk8"))
implementation(platform("org.jetbrains.kotlin:kotlin-bom:1.4.10"))
}
tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "14"
javaParameters = true
freeCompilerArgs = listOf(
"-Xinline-classes",
"-Xno-param-assertions",
"-Xno-call-assertions",
"-Xno-receiver-assertions"
)
}
}
@@ -0,0 +1,5 @@
package be.simplenotes
plugins {
kotlin("plugin.serialization")
}
@@ -0,0 +1,14 @@
META-INF/maven/**
META-INF/proguard/**
META-INF/*.kotlin_module
META-INF/DEPENDENCIES*
META-INF/NOTICE*
META-INF/LICENSE*
LICENSE*
META-INF/README*
META-INF/native-image/**
# 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/**
+1 -2
View File
@@ -2,8 +2,7 @@
"name": "css", "name": "css",
"version": "1.0.0", "version": "1.0.0",
"scripts": { "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", "//": "`gradle css`"
"css-purge": "NODE_ENV=production MANIFEST=../simplenotes-app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../simplenotes-app/src/main/resources/static/styles.css"
}, },
"dependencies": { "dependencies": {
"autoprefixer": "^9.8.6", "autoprefixer": "^9.8.6",
+1 -1
View File
@@ -1,7 +1,7 @@
module.exports = { module.exports = {
purge: { purge: {
content: [ content: [
'../simplenotes-app/src/main/kotlin/be/simplenotes/app/views/**/*.kt' process.env.PURGE
] ]
}, },
theme: { theme: {
-8
View File
@@ -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
Binary file not shown.
+5
View File
@@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
Vendored Executable
+185
View File
@@ -0,0 +1,185 @@
#!/usr/bin/env sh
#
# Copyright 2015 the original author or authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
##############################################################################
##
## Gradle start up script for UN*X
##
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
PRG="$0"
# Need this for relative symlinks.
while [ -h "$PRG" ] ; do
ls=`ls -ld "$PRG"`
link=`expr "$ls" : '.*-> \(.*\)$'`
if expr "$link" : '/.*' > /dev/null; then
PRG="$link"
else
PRG=`dirname "$PRG"`"/$link"
fi
done
SAVED="`pwd`"
cd "`dirname \"$PRG\"`/" >/dev/null
APP_HOME="`pwd -P`"
cd "$SAVED" >/dev/null
APP_NAME="Gradle"
APP_BASE_NAME=`basename "$0"`
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD="maximum"
warn () {
echo "$*"
}
die () {
echo
echo "$*"
echo
exit 1
}
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "`uname`" in
CYGWIN* )
cygwin=true
;;
Darwin* )
darwin=true
;;
MINGW* )
msys=true
;;
NONSTOP* )
nonstop=true
;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD="$JAVA_HOME/jre/sh/java"
else
JAVACMD="$JAVA_HOME/bin/java"
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD="java"
which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
# Increase the maximum file descriptors if we can.
if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
MAX_FD_LIMIT=`ulimit -H -n`
if [ $? -eq 0 ] ; then
if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
MAX_FD="$MAX_FD_LIMIT"
fi
ulimit -n $MAX_FD
if [ $? -ne 0 ] ; then
warn "Could not set maximum file descriptor limit: $MAX_FD"
fi
else
warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
fi
fi
# For Darwin, add options to specify how the application appears in the dock
if $darwin; then
GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
fi
# For Cygwin or MSYS, switch paths to Windows format before running java
if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
APP_HOME=`cygpath --path --mixed "$APP_HOME"`
CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
JAVACMD=`cygpath --unix "$JAVACMD"`
# We build the pattern for arguments to be converted via cygpath
ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
SEP=""
for dir in $ROOTDIRSRAW ; do
ROOTDIRS="$ROOTDIRS$SEP$dir"
SEP="|"
done
OURCYGPATTERN="(^($ROOTDIRS))"
# Add a user-defined pattern to the cygpath arguments
if [ "$GRADLE_CYGPATTERN" != "" ] ; then
OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
i=0
for arg in "$@" ; do
CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
else
eval `echo args$i`="\"$arg\""
fi
i=`expr $i + 1`
done
case $i in
0) set -- ;;
1) set -- "$args0" ;;
2) set -- "$args0" "$args1" ;;
3) set -- "$args0" "$args1" "$args2" ;;
4) set -- "$args0" "$args1" "$args2" "$args3" ;;
5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
esac
fi
# Escape application args
save () {
for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
echo " "
}
APP_ARGS=`save "$@"`
# Collect all arguments for the java command, following the shell quoting and substitution rules
eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
exec "$JAVACMD" "$@"
Vendored
+89
View File
@@ -0,0 +1,89 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@if "%DEBUG%" == "" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%" == "" set DIRNAME=.
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if "%ERRORLEVEL%" == "0" goto execute
echo.
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo.
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
echo.
echo Please set the JAVA_HOME variable in your environment to match the
echo location of your Java installation.
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if "%ERRORLEVEL%"=="0" goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
exit /b 1
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega
-249
View File
@@ -1,249 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>simplenotes-persistance</module>
<module>simplenotes-app</module>
<module>simplenotes-domain</module>
<module>simplenotes-search</module>
<module>simplenotes-types</module>
<module>simplenotes-config</module>
<module>simplenotes-test-resources</module>
</modules>
<packaging>pom</packaging>
<properties>
<java.version>14</java.version>
<kotlin.version>1.4.10</kotlin.version>
<junit.version>5.6.2</junit.version>
<kotlin.code.style>official</kotlin.code.style>
<kotlin.compiler.incremental>true</kotlin.compiler.incremental>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<kotlin.compiler.jvmTarget>${java.version}</kotlin.compiler.jvmTarget>
<org.mapstruct.version>1.4.1.Final</org.mapstruct.version>
</properties>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-stdlib-jdk8</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
<build>
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.2.0</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>3.2.4</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.0.0-M5</version>
<dependencies>
<dependency>
<groupId>org.apache.maven.surefire</groupId>
<artifactId>surefire-junit-platform</artifactId>
<version>3.0.0-M5</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-enforcer-plugin</artifactId>
<version>3.0.0-M3</version>
<executions>
<execution>
<id>enforce</id>
<goals>
<goal>enforce</goal>
</goals>
<configuration>
<rules>
<banDuplicatePomDependencyVersions/>
<requireMavenVersion>
<version>3.6</version>
</requireMavenVersion>
</rules>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<artifactId>kotlin-maven-plugin</artifactId>
<groupId>org.jetbrains.kotlin</groupId>
<version>${kotlin.version}</version>
<executions>
<execution>
<id>kapt</id>
<goals>
<goal>kapt</goal>
</goals>
<configuration>
<annotationProcessorPaths>
<annotationProcessorPath>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
</annotationProcessorPath>
</annotationProcessorPaths>
</configuration>
</execution>
<execution>
<id>compile</id>
<phase>process-sources</phase>
<goals>
<goal>compile</goal>
</goals>
</execution>
<execution>
<id>test-compile</id>
<goals>
<goal>test-compile</goal>
</goals>
</execution>
</executions>
<configuration>
<args>
<arg>-Xno-param-assertions</arg>
<arg>-Xno-call-assertions</arg>
<arg>-Xno-receiver-assertions</arg>
</args>
<compilerPlugins>
<plugin>kotlinx-serialization</plugin>
</compilerPlugins>
</configuration>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-maven-serialization</artifactId>
<version>${kotlin.version}</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-bom</artifactId>
<version>${kotlin.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json-jvm</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
<version>2.1.6</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<version>0.11.0</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>1.7.25</version>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-support-mysql</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
<version>${org.mapstruct.version}</version>
</dependency>
<!-- region tests -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<version>1.10.0</version>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<version>1.7.0.3</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<version>3.16.1</version>
</dependency>
<!-- endregion -->
</dependencies>
</dependencyManagement>
<repositories>
<repository>
<id>jcenter</id>
<url>https://jcenter.bintray.com</url>
</repository>
<repository>
<id>arrow</id>
<url>https://dl.bintray.com/arrow-kt/arrow-kt/</url>
</repository>
<repository>
<id>kotlinx</id>
<url>https://kotlin.bintray.com/kotlinx</url>
</repository>
</repositories>
</project>
+8
View File
@@ -0,0 +1,8 @@
rootProject.name = "simplenotes"
include(":simplenotes-config")
include(":simplenotes-views")
include(":simplenotes-app")
include(":simplenotes-domain")
include(":simplenotes-search")
include(":simplenotes-types")
include(":simplenotes-persistance")
+39
View File
@@ -0,0 +1,39 @@
import be.simplenotes.Libs
plugins {
id("be.simplenotes.base")
id("be.simplenotes.kotlinx-serialization")
id("be.simplenotes.app-shadow")
id("be.simplenotes.app-css")
id("be.simplenotes.app-docker")
kotlin("kapt")
}
dependencies {
implementation(project(":simplenotes-persistance"))
implementation(project(":simplenotes-search"))
implementation(project(":simplenotes-domain"))
implementation(project(":simplenotes-types"))
implementation(project(":simplenotes-config"))
implementation(project(":simplenotes-views"))
implementation(Libs.arrowCoreData)
implementation(Libs.konform)
implementation(Libs.http4kCore)
implementation(Libs.jettyServer)
implementation(Libs.jettyServlet)
implementation(Libs.javaxServlet)
implementation(Libs.kotlinxSerializationJson)
implementation(Libs.logbackClassic)
implementation(Libs.ktormCore)
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
testImplementation(Libs.micronaut)
kaptTest(Libs.micronautProcessor)
testImplementation(Libs.junit)
testImplementation(Libs.assertJ)
testImplementation(Libs.http4kTestingHamkrest)
}
-207
View File
@@ -1,207 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simplenotes-parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-app</artifactId>
<properties>
<http4k.version>3.268.0</http4k.version>
</properties>
<dependencies>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-persistance</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-search</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-domain</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-core</artifactId>
</dependency>
<!--
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-server-jetty</artifactId>
</dependency>
-->
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-server</artifactId>
<version>9.4.32.v20200930</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.eclipse.jetty</groupId>
<artifactId>jetty-servlet</artifactId>
<version>9.4.32.v20200930</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>4.0.1</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-html-jvm</artifactId>
<version>0.7.1</version>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json-jvm</artifactId>
</dependency>
<dependency>
<groupId>org.ocpsoft.prettytime</groupId>
<artifactId>prettytime</artifactId>
<version>4.0.5.Final</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-test-resources</artifactId>
<version>1.0-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-testing-hamkrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.http4k</groupId>
<artifactId>http4k-bom</artifactId>
<version>${http4k.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
<configuration>
<minimizeJar>true</minimizeJar>
<transformers>
<transformer
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>be.simplenotes.app.SimpleNotesKt</mainClass>
</transformer>
</transformers>
<filters>
<filter>
<artifact>com.h2database:h2</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>org.mariadb.jdbc:mariadb-java-client</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>org.jetbrains.kotlin:kotlin-reflect</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>org.eclipse.jetty:*</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>org.apache.lucene:*</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>org.ocpsoft.prettytime:prettytime</artifact>
<includes>
<include>**</include>
</includes>
</filter>
<filter>
<artifact>*:*</artifact>
<excludes>
<exclude>META-INF/maven/**</exclude>
<exclude>META-INF/proguard/**</exclude>
<exclude>META-INF/*.kotlin_module</exclude>
<exclude>META-INF/DEPENDENCIES*</exclude>
<exclude>META-INF/NOTICE*</exclude>
<exclude>META-INF/LICENSE*</exclude>
<exclude>LICENSE*</exclude>
<exclude>META-INF/README*</exclude>
<exclude>META-INF/native-image/**</exclude>
</excludes>
</filter>
</filters>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
@@ -1,21 +1,27 @@
package be.simplenotes.app package be.simplenotes.app
import io.micronaut.context.annotation.Context
import org.http4k.server.Http4kServer import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
@Context
class Server( class Server(
private val config: SimpleNotesServerConfig, private val config: SimpleNotesServerConfig,
private val http4kServer: Http4kServer, private val http4kServer: Http4kServer,
) { ) {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
@PostConstruct
fun start(): Server { fun start(): Server {
http4kServer.start() http4kServer.start()
logger.info("Listening on http://${config.host}:${config.port}") logger.info("Listening on http://${config.host}:${config.port}")
return this return this
} }
@PreDestroy
fun stop() { fun stop() {
logger.info("Stopping server") logger.info("Stopping server")
http4kServer.close() http4kServer.close()
@@ -1,31 +1,12 @@
package be.simplenotes.app package be.simplenotes.app
import be.simplenotes.app.extensions.addShutdownHook import io.micronaut.context.ApplicationContext
import be.simplenotes.app.modules.*
import be.simplenotes.domain.domainModule
import be.simplenotes.persistance.migrationModule
import be.simplenotes.persistance.persistanceModule
import be.simplenotes.search.searchModule
import org.koin.core.context.startKoin
import org.koin.core.context.unloadKoinModules
fun main() { fun main() {
startKoin { val ctx = ApplicationContext.run().start()
modules( Runtime.getRuntime().addShutdownHook(
serverModule, Thread {
persistanceModule, ctx.stop()
migrationModule, }
configModule, )
baseModule,
userModule,
noteModule,
settingsModule,
domainModule,
searchModule,
apiModule,
jsonModule
)
}.addShutdownHook()
unloadKoinModules(listOf(migrationModule, configModule))
} }
@@ -2,10 +2,10 @@ package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto import be.simplenotes.app.extensions.auto
import be.simplenotes.app.utils.parseSearchTerms import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.PersistedNote import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.NoteService
import kotlinx.serialization.Contextual import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@@ -17,41 +17,49 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.lens.Path import org.http4k.lens.Path
import org.http4k.lens.uuid import org.http4k.lens.uuid
import java.util.* import java.util.*
import javax.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) val content = noteContentLens(request)
return noteService.create(jwtPayload.userId, content).fold( return noteService.create(loggedInUser.userId, content).fold(
{ Response(BAD_REQUEST) }, { Response(BAD_REQUEST) },
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) } { uuidContentLens(UuidContent(it.uuid), Response(OK)) }
) )
} }
fun notes(request: Request, jwtPayload: JwtPayload): Response { fun notes(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser): Response {
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes val notes = noteService.paginatedNotes(loggedInUser.userId, page = 1).notes
return persistedNotesMetadataLens(notes, Response(OK)) return persistedNotesMetadataLens(notes, Response(OK))
} }
fun note(request: Request, jwtPayload: JwtPayload): Response = fun note(request: Request, loggedInUser: LoggedInUser): Response =
noteService.find(jwtPayload.userId, uuidLens(request)) noteService.find(loggedInUser.userId, uuidLens(request))
?.let { persistedNoteLens(it, Response(OK)) } ?.let { persistedNoteLens(it, Response(OK)) }
?: Response(NOT_FOUND) ?: Response(NOT_FOUND)
fun update(request: Request, jwtPayload: JwtPayload): Response { fun update(request: Request, loggedInUser: LoggedInUser): Response {
val content = noteContentLens(request) val content = noteContentLens(request)
return noteService.update(jwtPayload.userId, uuidLens(request), content).fold({ return noteService.update(loggedInUser.userId, uuidLens(request), content).fold(
Response(BAD_REQUEST) {
}, { Response(BAD_REQUEST)
if (it == null) Response(NOT_FOUND) },
else Response(OK) {
}) 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 query = searchContentLens(request)
val terms = parseSearchTerms(query) val terms = parseSearchTerms(query)
val notes = noteService.search(jwtPayload.userId, terms) val notes = noteService.search(loggedInUser.userId, terms)
return persistedNotesMetadataLens(notes, Response(OK)) return persistedNotesMetadataLens(notes, Response(OK))
} }
@@ -61,7 +69,6 @@ class ApiNoteController(private val noteService: NoteService, private val json:
private val persistedNotesMetadataLens = json.auto<List<PersistedNoteMetadata>>().toLens() private val persistedNotesMetadataLens = json.auto<List<PersistedNoteMetadata>>().toLens()
private val persistedNoteLens = json.auto<PersistedNote>().toLens() private val persistedNoteLens = json.auto<PersistedNote>().toLens()
private val uuidLens = Path.uuid().of("uuid") private val uuidLens = Path.uuid().of("uuid")
} }
@Serializable @Serializable
@@ -9,8 +9,13 @@ import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import javax.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 tokenLens = json.auto<Token>().toLens()
private val loginFormLens = json.auto<LoginForm>().toLens() private val loginFormLens = json.auto<LoginForm>().toLens()
@@ -1,13 +1,15 @@
package be.simplenotes.app.controllers package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.BaseView import be.simplenotes.types.LoggedInUser
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.views.BaseView
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import javax.inject.Singleton
@Singleton
class BaseController(private val view: BaseView) { class BaseController(private val view: BaseView) {
fun index(@Suppress("UNUSED_PARAMETER") request: Request, jwtPayload: JwtPayload?) = fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) =
Response(OK).html(view.renderHome(jwtPayload)) Response(OK).html(view.renderHome(loggedInUser))
} }
@@ -5,8 +5,10 @@ import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import javax.inject.Singleton
@Singleton
class HealthCheckController(private val dbHealthCheck: DbHealthCheck) { class HealthCheckController(private val dbHealthCheck: DbHealthCheck) {
fun healthCheck(request: Request) = fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE) if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
} }
@@ -3,12 +3,12 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect import be.simplenotes.app.extensions.redirect
import be.simplenotes.app.utils.parseSearchTerms import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.app.views.NoteView
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.NoteService import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.usecases.markdown.InvalidMeta import be.simplenotes.domain.usecases.markdown.InvalidMeta
import be.simplenotes.domain.usecases.markdown.MissingMeta import be.simplenotes.domain.usecases.markdown.MissingMeta
import be.simplenotes.domain.usecases.markdown.ValidationError import be.simplenotes.domain.usecases.markdown.ValidationError
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.NoteView
import org.http4k.core.Method import org.http4k.core.Method
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
@@ -18,25 +18,35 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.core.body.form import org.http4k.core.body.form
import org.http4k.routing.path import org.http4k.routing.path
import java.util.* import java.util.*
import javax.inject.Singleton
import kotlin.math.abs import kotlin.math.abs
@Singleton
class NoteController( class NoteController(
private val view: NoteView, private val view: NoteView,
private val noteService: NoteService, private val noteService: NoteService,
) { ) {
fun new(request: Request, jwtPayload: JwtPayload): Response { fun new(request: Request, loggedInUser: LoggedInUser): Response {
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(jwtPayload)) if (request.method == Method.GET) return Response(OK).html(view.noteEditor(loggedInUser))
val markdownForm = request.form("markdown") ?: "" val markdownForm = request.form("markdown") ?: ""
return noteService.create(jwtPayload.userId, markdownForm).fold( return noteService.create(loggedInUser.userId, markdownForm).fold(
{ {
val html = when (it) { val html = when (it) {
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm) MissingMeta -> view.noteEditor(
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm) loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
)
InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
)
is ValidationError -> view.noteEditor( is ValidationError -> view.noteEditor(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors, validationErrors = it.validationErrors,
textarea = markdownForm textarea = markdownForm
) )
@@ -49,66 +59,74 @@ class NoteController(
) )
} }
fun list(request: Request, jwtPayload: JwtPayload): Response { fun list(request: Request, loggedInUser: LoggedInUser): Response {
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1 val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
val tag = request.query("tag") val tag = request.query("tag")
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag) val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag)
val deletedCount = noteService.countDeleted(jwtPayload.userId) val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages, deletedCount, tag = tag)) return Response(OK).html(view.notes(loggedInUser, notes, currentPage, pages, deletedCount, tag = tag))
} }
fun search(request: Request, jwtPayload: JwtPayload): Response { fun search(request: Request, loggedInUser: LoggedInUser): Response {
val query = request.form("search") ?: "" val query = request.form("search") ?: ""
val terms = parseSearchTerms(query) val terms = parseSearchTerms(query)
val notes = noteService.search(jwtPayload.userId, terms) val notes = noteService.search(loggedInUser.userId, terms)
val deletedCount = noteService.countDeleted(jwtPayload.userId) val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.search(jwtPayload, notes, query, deletedCount)) return Response(OK).html(view.search(loggedInUser, notes, query, deletedCount))
} }
fun note(request: Request, jwtPayload: JwtPayload): Response { fun note(request: Request, loggedInUser: LoggedInUser): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND) val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
if (request.method == Method.POST) { if (request.method == Method.POST) {
if (request.form("delete") != null) { if (request.form("delete") != null) {
return if (noteService.trash(jwtPayload.userId, noteUuid)) return if (noteService.trash(loggedInUser.userId, noteUuid))
Response.redirect("/notes") // TODO: flash cookie to show success ? Response.redirect("/notes") // TODO: flash cookie to show success ?
else else
Response(NOT_FOUND) // TODO: show an error Response(NOT_FOUND) // TODO: show an error
} }
if (request.form("public") != null) { if (request.form("public") != null) {
if (!noteService.makePublic(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND) if (!noteService.makePublic(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
} else if (request.form("private") != null) { } else if (request.form("private") != null) {
if (!noteService.makePrivate(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND) if (!noteService.makePrivate(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
} }
} }
val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND) val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
return Response(OK).html(view.renderedNote(jwtPayload, note, shared = false)) return Response(OK).html(view.renderedNote(loggedInUser, note, shared = false))
} }
fun public(request: Request, jwtPayload: JwtPayload?): Response { fun public(request: Request, loggedInUser: LoggedInUser?): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND) val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
val note = noteService.findPublic(noteUuid) ?: return Response(NOT_FOUND) val note = noteService.findPublic(noteUuid) ?: return Response(NOT_FOUND)
return Response(OK).html(view.renderedNote(jwtPayload, note, shared = true)) return Response(OK).html(view.renderedNote(loggedInUser, note, shared = true))
} }
fun edit(request: Request, jwtPayload: JwtPayload): Response { fun edit(request: Request, loggedInUser: LoggedInUser): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND) val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND) val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
if (request.method == Method.GET) { if (request.method == Method.GET) {
return Response(OK).html(view.noteEditor(jwtPayload, textarea = note.markdown)) return Response(OK).html(view.noteEditor(loggedInUser, textarea = note.markdown))
} }
val markdownForm = request.form("markdown") ?: "" val markdownForm = request.form("markdown") ?: ""
return noteService.update(jwtPayload.userId, note.uuid, markdownForm).fold( return noteService.update(loggedInUser.userId, note.uuid, markdownForm).fold(
{ {
val html = when (it) { val html = when (it) {
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm) MissingMeta -> view.noteEditor(
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm) loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
)
InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
)
is ValidationError -> view.noteEditor( is ValidationError -> view.noteEditor(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors, validationErrors = it.validationErrors,
textarea = markdownForm textarea = markdownForm
) )
@@ -121,21 +139,21 @@ class NoteController(
) )
} }
fun trash(request: Request, jwtPayload: JwtPayload): Response { fun trash(request: Request, loggedInUser: LoggedInUser): Response {
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1 val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
val tag = request.query("tag") val tag = request.query("tag")
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag, deleted = true) val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag, deleted = true)
return Response(OK).html(view.trash(jwtPayload, notes, currentPage, pages)) return Response(OK).html(view.trash(loggedInUser, notes, currentPage, pages))
} }
fun deleted(request: Request, jwtPayload: JwtPayload): Response { fun deleted(request: Request, loggedInUser: LoggedInUser): Response {
val uuid = request.uuidPath() ?: return Response(NOT_FOUND) val uuid = request.uuidPath() ?: return Response(NOT_FOUND)
return if (request.form("delete") != null) return if (request.form("delete") != null)
if (noteService.delete(jwtPayload.userId, uuid)) if (noteService.delete(loggedInUser.userId, uuid))
Response.redirect("/notes/trash") Response.redirect("/notes/trash")
else else
Response(NOT_FOUND) Response(NOT_FOUND)
else if (noteService.restore(jwtPayload.userId, uuid)) else if (noteService.restore(loggedInUser.userId, uuid))
Response.redirect("/notes/$uuid") Response.redirect("/notes/$uuid")
else else
Response(NOT_FOUND) Response(NOT_FOUND)
@@ -2,24 +2,26 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect 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.UserService
import be.simplenotes.domain.usecases.users.delete.DeleteError import be.simplenotes.domain.usecases.users.delete.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm import be.simplenotes.domain.usecases.users.delete.DeleteForm
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.SettingView
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.body.form import org.http4k.core.body.form
import org.http4k.core.cookie.invalidateCookie import org.http4k.core.cookie.invalidateCookie
import javax.inject.Singleton
@Singleton
class SettingsController( class SettingsController(
private val userService: UserService, private val userService: UserService,
private val settingView: SettingView, private val settingView: SettingView,
) { ) {
fun settings(request: Request, jwtPayload: JwtPayload): Response { fun settings(request: Request, loggedInUser: LoggedInUser): Response {
if (request.method == Method.GET) 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) val result = userService.delete(deleteForm)
return result.fold( return result.fold(
@@ -28,13 +30,13 @@ class SettingsController(
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer") DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer")
DeleteError.WrongPassword -> Response(Status.OK).html( DeleteError.WrongPassword -> Response(Status.OK).html(
settingView.settings( settingView.settings(
jwtPayload, loggedInUser,
error = "Wrong password" error = "Wrong password"
) )
) )
is DeleteError.InvalidForm -> Response(Status.OK).html( is DeleteError.InvalidForm -> Response(Status.OK).html(
settingView.settings( settingView.settings(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors
) )
) )
@@ -53,23 +55,26 @@ class SettingsController(
.header("Content-Type", contentType) .header("Content-Type", contentType)
} }
fun export(request: Request, jwtPayload: JwtPayload): Response { fun export(request: Request, loggedInUser: LoggedInUser): Response {
val isDownload = request.form("download") != null val isDownload = request.form("download") != null
return if (isDownload) { return if (isDownload) {
val filename = "simplenotes-export-${jwtPayload.username}" val filename = "simplenotes-export-${loggedInUser.username}"
if (request.form("format") == "zip") { if (request.form("format") == "zip") {
val zip = userService.exportAsZip(jwtPayload.userId) val zip = userService.exportAsZip(loggedInUser.userId)
Response(Status.OK) Response(Status.OK)
.with(attachment("$filename.zip", "application/zip")) .with(attachment("$filename.zip", "application/zip"))
.body(zip) .body(zip)
} else } else
Response(Status.OK) Response(Status.OK)
.with(attachment("$filename.json", "application/json")) .with(attachment("$filename.json", "application/json"))
.body(userService.exportAsJson(jwtPayload.userId)) .body(userService.exportAsJson(loggedInUser.userId))
} else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json") } else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header(
"Content-Type",
"application/json"
)
} }
private fun Request.deleteForm(jwtPayload: JwtPayload) = private fun Request.deleteForm(loggedInUser: LoggedInUser) =
DeleteForm(jwtPayload.username, form("password"), form("checked") != null) DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
} }
@@ -3,14 +3,14 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.isSecure import be.simplenotes.app.extensions.isSecure
import be.simplenotes.app.extensions.redirect import be.simplenotes.app.extensions.redirect
import be.simplenotes.app.views.UserView import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.UserService import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.* import be.simplenotes.domain.usecases.users.login.*
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
import be.simplenotes.domain.usecases.users.register.RegisterForm import be.simplenotes.domain.usecases.users.register.RegisterForm
import be.simplenotes.domain.usecases.users.register.UserExists import be.simplenotes.domain.usecases.users.register.UserExists
import be.simplenotes.config.JwtConfig import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.UserView
import org.http4k.core.Method.GET import org.http4k.core.Method.GET
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Response import org.http4k.core.Response
@@ -21,15 +21,17 @@ import org.http4k.core.cookie.SameSite
import org.http4k.core.cookie.cookie import org.http4k.core.cookie.cookie
import org.http4k.core.cookie.invalidateCookie import org.http4k.core.cookie.invalidateCookie
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Singleton
class UserController( class UserController(
private val userService: UserService, private val userService: UserService,
private val userView: UserView, private val userView: UserView,
private val jwtConfig: JwtConfig, 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( if (request.method == GET) return Response(OK).html(
userView.register(jwtPayload) userView.register(loggedInUser)
) )
val result = userService.register(request.registerForm()) val result = userService.register(request.registerForm())
@@ -38,12 +40,12 @@ class UserController(
{ {
val html = when (it) { val html = when (it) {
UserExists -> userView.register( UserExists -> userView.register(
jwtPayload, loggedInUser,
error = "User already exists" error = "User already exists"
) )
is InvalidRegisterForm -> is InvalidRegisterForm ->
userView.register( userView.register(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors
) )
} }
@@ -58,9 +60,9 @@ class UserController(
private fun Request.registerForm() = RegisterForm(form("username"), form("password")) private fun Request.registerForm() = RegisterForm(form("username"), form("password"))
private fun Request.loginForm(): LoginForm = registerForm() 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( if (request.method == GET) return Response(OK).html(
userView.login(jwtPayload) userView.login(loggedInUser)
) )
val result = userService.login(request.loginForm()) val result = userService.login(request.loginForm())
@@ -70,17 +72,17 @@ class UserController(
val html = when (it) { val html = when (it) {
Unregistered -> Unregistered ->
userView.login( userView.login(
jwtPayload, loggedInUser,
error = "User does not exist" error = "User does not exist"
) )
WrongPassword -> WrongPassword ->
userView.login( userView.login(
jwtPayload, loggedInUser,
error = "Wrong password" error = "Wrong password"
) )
is InvalidLoginForm -> is InvalidLoginForm ->
userView.login( userView.login(
jwtPayload, loggedInUser,
validationErrors = it.validationErrors validationErrors = it.validationErrors
) )
} }
@@ -23,7 +23,8 @@ fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
val bodyLens = httpBodyRoot( val bodyLens = httpBodyRoot(
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")), listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
ContentType.APPLICATION_JSON.withNoDirectives(), ContentNegotiation.StrictNoDirective ContentType.APPLICATION_JSON.withNoDirectives(),
ContentNegotiation.StrictNoDirective
).map( ).map(
{ it.payload.asString() }, { it.payload.asString() },
{ Body(it) } { Body(it) }
@@ -1,12 +0,0 @@
package be.simplenotes.app.extensions
import org.koin.core.KoinApplication
import kotlin.concurrent.thread
fun KoinApplication.addShutdownHook() {
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
close()
}
)
}
@@ -1,15 +0,0 @@
package be.simplenotes.app.extensions
import kotlinx.html.*
class SUMMARY(consumer: TagConsumer<*>) :
HTMLTag(
"summary", consumer, emptyMap(),
inlineTag = true,
emptyTag = false
),
HtmlInlineTag
fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) {
SUMMARY(consumer).visit(block)
}
@@ -1,58 +0,0 @@
package be.simplenotes.app.filters
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.JwtPayloadExtractor
import org.http4k.core.*
import org.http4k.core.Status.Companion.UNAUTHORIZED
import org.http4k.core.cookie.cookie
enum class AuthType {
Optional, Required
}
private const val authKey = "auth"
class AuthFilter(
private val extractor: JwtPayloadExtractor,
private val authType: AuthType,
private val ctx: RequestContexts,
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 { token -> extractor(token) }
when {
jwtPayload != null -> {
ctx[it][authKey] = jwtPayload
next(it)
}
authType == AuthType.Required -> {
if (redirect) Response.redirect("/login")
else Response(UNAUTHORIZED)
}
else -> next(it)
}
}
}
fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey]
enum class JwtSource {
Header, Cookie
}
private fun Request.bearerTokenCookie(): String? = cookie("Bearer")
?.value
?.trim()
private fun Request.bearerTokenHeader(): String? =
header("Authorization")
?.trim()
?.takeIf { it.startsWith("Bearer") }
?.substringAfter("Bearer")
?.trim()
@@ -1,8 +1,8 @@
package be.simplenotes.app.filters package be.simplenotes.app.filters
import be.simplenotes.app.extensions.html import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.ErrorView import be.simplenotes.views.ErrorView
import be.simplenotes.app.views.ErrorView.Type.* import be.simplenotes.views.ErrorView.Type.*
import org.http4k.core.* import org.http4k.core.*
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
import org.http4k.core.Status.Companion.NOT_FOUND 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.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import org.slf4j.LoggerFactory import org.slf4j.LoggerFactory
import java.sql.SQLTransientException import java.sql.SQLTransientException
import javax.inject.Singleton
@Singleton
class ErrorFilter(private val errorView: ErrorView) : Filter { class ErrorFilter(private val errorView: ErrorView) : Filter {
private val logger = LoggerFactory.getLogger(javaClass) private val logger = LoggerFactory.getLogger(javaClass)
@@ -3,9 +3,13 @@ package be.simplenotes.app.filters
import org.http4k.core.Filter import org.http4k.core.Filter
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import org.http4k.core.Request import org.http4k.core.Request
import org.http4k.core.Status.Companion.OK
object ImmutableFilter : Filter { object ImmutableFilter : Filter {
override fun invoke(next: HttpHandler) = { request: Request -> 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
} }
} }
@@ -3,7 +3,9 @@ package be.simplenotes.app.filters
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import org.http4k.core.Filter import org.http4k.core.Filter
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import javax.inject.Singleton
@Singleton
class TransactionFilter(private val db: Database) : Filter { class TransactionFilter(private val db: Database) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = { request -> override fun invoke(next: HttpHandler): HttpHandler = { request ->
db.useTransaction { db.useTransaction {
@@ -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,22 @@
package be.simplenotes.app.filters.auth
import be.simplenotes.app.filters.auth.JwtSource.Cookie
import be.simplenotes.domain.security.JwtPayloadExtractor
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.with
class OptionalAuthFilter(
private val extractor: JwtPayloadExtractor,
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 { extractor(it) }))
}
}
@@ -0,0 +1,30 @@
package be.simplenotes.app.filters.auth
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.security.JwtPayloadExtractor
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 extractor: JwtPayloadExtractor,
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 { extractor(token) }
if (jwtPayload != null) next(it.with(lens of jwtPayload))
else {
if (redirect) Response.redirect("/login")
else Response(UNAUTHORIZED)
}
}
}
@@ -12,9 +12,12 @@ import org.http4k.servlet.asServlet
class Jetty(private val port: Int, private val server: Server) : ServerConfig { class Jetty(private val port: Int, private val server: Server) : ServerConfig {
constructor(port: Int = 8000) : this(port, http(port)) 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(
inConnectors.forEach { addConnector(it(this)) } port,
}) Server().apply {
inConnectors.forEach { addConnector(it(this)) }
}
)
override fun toServer(httpHandler: HttpHandler): Http4kServer { override fun toServer(httpHandler: HttpHandler): Http4kServer {
server.insertHandler(httpHandler.toJettyHandler()) server.insertHandler(httpHandler.toJettyHandler())
@@ -1,24 +0,0 @@
package be.simplenotes.app.modules
import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.JwtSource
import org.http4k.core.Filter
import org.koin.core.qualifier.named
import org.koin.dsl.module
val apiModule = module {
single { ApiUserController(get(), get()) }
single { ApiNoteController(get(), get()) }
single<Filter>(named("apiAuthFilter")) {
AuthFilter(
extractor = get(),
authType = AuthType.Required,
ctx = get(),
source = JwtSource.Header,
redirect = false
)
}
}
@@ -0,0 +1,46 @@
package be.simplenotes.app.modules
import be.simplenotes.app.filters.auth.*
import be.simplenotes.domain.security.JwtPayloadExtractor
import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Primary
import org.http4k.core.RequestContexts
import org.http4k.lens.RequestContextKey
import javax.inject.Named
import javax.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(extractor: JwtPayloadExtractor, @Named("optional") lens: OptionalAuthLens) =
OptionalAuthFilter(extractor, lens)
@Primary
@Singleton
fun requiredAuth(extractor: JwtPayloadExtractor, @Named("required") lens: RequiredAuthLens) =
RequiredAuthFilter(extractor, lens)
@Singleton
@Named("api")
internal fun apiAuthFilter(
jwtPayloadExtractor: JwtPayloadExtractor,
@Named("required") lens: RequiredAuthLens,
) = RequiredAuthFilter(
extractor = jwtPayloadExtractor,
lens = lens,
source = JwtSource.Header,
redirect = false
)
@Singleton
fun requestContexts() = RequestContexts()
}
@@ -1,11 +0,0 @@
package be.simplenotes.app.modules
import be.simplenotes.app.Config
import org.koin.dsl.module
val configModule = module {
single { Config() }
single { get<Config>().dataSourceConfig }
single { get<Config>().jwtConfig }
single { get<Config>().serverConfig }
}
@@ -1,29 +0,0 @@
package be.simplenotes.app.modules
import be.simplenotes.app.controllers.*
import be.simplenotes.app.views.BaseView
import be.simplenotes.app.views.NoteView
import be.simplenotes.app.views.SettingView
import be.simplenotes.app.views.UserView
import org.koin.dsl.module
val userModule = module {
single { UserController(get(), get(), get()) }
single { UserView(get()) }
}
val baseModule = module {
single { HealthCheckController(get()) }
single { BaseController(get()) }
single { BaseView(get()) }
}
val noteModule = module {
single { NoteController(get(), get()) }
single { NoteView(get()) }
}
val settingsModule = module {
single { SettingsController(get(), get()) }
single { SettingView(get()) }
}
@@ -2,21 +2,20 @@ package be.simplenotes.app.modules
import be.simplenotes.app.serialization.LocalDateTimeSerializer import be.simplenotes.app.serialization.LocalDateTimeSerializer
import be.simplenotes.app.serialization.UuidSerializer import be.simplenotes.app.serialization.UuidSerializer
import io.micronaut.context.annotation.Factory
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule import kotlinx.serialization.modules.SerializersModule
import org.koin.dsl.module
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
import javax.inject.Singleton
val jsonModule = module { @Factory
single { class JsonModule {
Json {
prettyPrint = true @Singleton
serializersModule = get() fun json() = Json {
} prettyPrint = true
} serializersModule = SerializersModule {
single {
SerializersModule {
contextual(LocalDateTime::class, LocalDateTimeSerializer()) contextual(LocalDateTime::class, LocalDateTimeSerializer())
contextual(UUID::class, UuidSerializer()) contextual(UUID::class, UuidSerializer())
} }
@@ -1,63 +1,38 @@
package be.simplenotes.app.modules package be.simplenotes.app.modules
import be.simplenotes.app.Server
import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.jetty.ConnectorBuilder import be.simplenotes.app.jetty.ConnectorBuilder
import be.simplenotes.app.jetty.Jetty import be.simplenotes.app.jetty.Jetty
import be.simplenotes.app.routes.Router import be.simplenotes.app.routes.Router
import be.simplenotes.app.utils.StaticFileResolver import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.utils.StaticFileResolverImpl
import be.simplenotes.app.views.ErrorView
import be.simplenotes.config.ServerConfig import be.simplenotes.config.ServerConfig
import io.micronaut.context.annotation.Factory
import org.eclipse.jetty.server.ServerConnector import org.eclipse.jetty.server.ServerConnector
import org.http4k.core.Filter import org.http4k.server.Http4kServer
import org.http4k.core.RequestContexts
import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.asServer import org.http4k.server.asServer
import org.koin.core.qualifier.named import javax.inject.Named
import org.koin.core.qualifier.qualifier import javax.inject.Singleton
import org.koin.dsl.module import org.eclipse.jetty.server.Server as JettyServer
import org.koin.dsl.onClose
import org.http4k.server.ServerConfig as Http4kServerConfig import org.http4k.server.ServerConfig as Http4kServerConfig
val serverModule = module { @Factory
single(createdAtStart = true) { Server(get(), get()).start() } onClose { it?.stop() } class ServerModule {
single { get<RoutingHttpHandler>().asServer(get()) }
single<Http4kServerConfig> { @Singleton
val config = get<ServerConfig>() @Named("styles")
val builder: ConnectorBuilder = { server: org.eclipse.jetty.server.Server -> 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 { ServerConnector(server).apply {
port = config.port port = config.port
host = config.host host = config.host
} }
} }
Jetty(config.port, builder) return Jetty(config.port, builder)
} }
single<StaticFileResolver> { StaticFileResolverImpl(get()) }
single {
Router(
get(),
get(),
get(),
get(),
get(),
get(),
get(),
requiredAuth = get(AuthType.Required.qualifier),
optionalAuth = get(AuthType.Optional.qualifier),
apiAuth = get(named("apiAuthFilter")),
get(),
get(),
get(),
)()
}
single { RequestContexts() }
single<Filter>(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get()) }
single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) }
single { ErrorFilter(get()) }
single { TransactionFilter(get()) }
single { ErrorView(get()) }
} }
@@ -0,0 +1,54 @@
package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
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 javax.inject.Named
import javax.inject.Singleton
@Singleton
class ApiRoutes(
private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController,
private val transaction: TransactionFilter,
@Named("api") private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
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 transaction.then(::createNote),
"/search" bind POST to ::search,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind PUT to transaction.then(::note),
)
).withBasePath("/notes")
}
).withBasePath("/api")
}
}
@@ -0,0 +1,67 @@
package be.simplenotes.app.routes
import be.simplenotes.app.controllers.BaseController
import be.simplenotes.app.controllers.HealthCheckController
import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.controllers.UserController
import be.simplenotes.app.filters.ImmutableFilter
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.filters.auth.OptionalAuthFilter
import be.simplenotes.app.filters.auth.OptionalAuthLens
import org.http4k.core.ContentType
import org.http4k.core.Filter
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 javax.inject.Named
import javax.inject.Singleton
@Singleton
class BasicRoutes(
private val healthCheckController: HealthCheckController,
private val baseCtrl: BaseController,
private val userCtrl: UserController,
private val noteCtrl: NoteController,
@Named("optional") private val authLens: OptionalAuthLens,
private val auth: OptionalAuthFilter,
private val transactionFilter: TransactionFilter,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
fun Filter.then(next: PublicHandler) = then { req: Request ->
next(req, authLens(req))
}
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 transactionFilter.then(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,
)
),
"/health" bind GET to healthCheckController::healthCheck,
staticHandler
)
}
}
@@ -0,0 +1,53 @@
package be.simplenotes.app.routes
import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
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 javax.inject.Named
import javax.inject.Singleton
@Singleton
class NoteRoutes(
private val noteCtrl: NoteController,
private val transaction: TransactionFilter,
private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
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 transaction.then(::new),
"/trash" bind GET to ::trash,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind POST to transaction.then(::note),
"/{uuid}/edit" bind GET to ::edit,
"/{uuid}/edit" bind POST to transaction.then(::edit),
"/deleted/{uuid}" bind POST to transaction.then(::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
@@ -1,106 +1,32 @@
package be.simplenotes.app.routes package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.api.ApiUserController import be.simplenotes.app.filters.SecurityFilter
import be.simplenotes.app.controllers.* import org.http4k.core.RequestContexts
import be.simplenotes.app.filters.* import org.http4k.core.then
import be.simplenotes.domain.security.JwtPayload
import org.http4k.core.*
import org.http4k.core.Method.*
import org.http4k.filter.ResponseFilters.GZip import org.http4k.filter.ResponseFilters.GZip
import org.http4k.filter.ServerFilters.InitialiseRequestContext import org.http4k.filter.ServerFilters.InitialiseRequestContext
import org.http4k.routing.* import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.ResourceLoader.Companion.Classpath import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Singleton
@Singleton
class Router( class Router(
private val baseController: BaseController,
private val userController: UserController,
private val noteController: NoteController,
private val settingsController: SettingsController,
private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController,
private val healthCheckController: HealthCheckController,
private val requiredAuth: Filter,
private val optionalAuth: Filter,
private val apiAuth: Filter,
private val errorFilter: ErrorFilter, private val errorFilter: ErrorFilter,
private val transactionFilter: TransactionFilter,
private val contexts: RequestContexts, private val contexts: RequestContexts,
private val subRouters: List<Supplier<RoutingHttpHandler>>,
) { ) {
operator fun invoke(): RoutingHttpHandler { operator fun invoke(): RoutingHttpHandler {
val basicRoutes =
routes(
"/health" bind GET to healthCheckController::healthCheck,
ImmutableFilter.then(static(Classpath("/static"), "woff2" to ContentType("font/woff2")))
)
val publicRoutes = routes(
"/" bind GET public baseController::index,
"/register" bind GET public userController::register,
"/register" bind POST `public transactional` userController::register,
"/login" bind GET public userController::login,
"/login" bind POST public userController::login,
"/logout" bind POST to userController::logout,
"/notes/public/{uuid}" bind GET public noteController::public,
)
val protectedRoutes = routes(
"/settings" bind GET protected settingsController::settings,
"/settings" bind POST transactional settingsController::settings,
"/export" bind POST protected settingsController::export,
"/notes" bind GET protected noteController::list,
"/notes" bind POST protected noteController::search,
"/notes/new" bind GET protected noteController::new,
"/notes/new" bind POST transactional noteController::new,
"/notes/trash" bind GET protected noteController::trash,
"/notes/{uuid}" bind GET protected noteController::note,
"/notes/{uuid}" bind POST transactional noteController::note,
"/notes/{uuid}/edit" bind GET protected noteController::edit,
"/notes/{uuid}/edit" bind POST transactional noteController::edit,
"/notes/deleted/{uuid}" bind POST transactional noteController::deleted,
)
val apiRoutes = routes(
"/api/login" bind POST to apiUserController::login,
)
val protectedApiRoutes = routes(
"/api/notes" bind GET protected apiNoteController::notes,
"/api/notes" bind POST transactional apiNoteController::createNote,
"/api/notes/search" bind POST transactional apiNoteController::search,
"/api/notes/{uuid}" bind GET protected apiNoteController::note,
"/api/notes/{uuid}" bind PUT transactional apiNoteController::update,
)
val routes = routes( val routes = routes(
basicRoutes, *subRouters.map { it.get() }.toTypedArray()
optionalAuth.then(publicRoutes),
requiredAuth.then(protectedRoutes),
apiAuth.then(protectedApiRoutes),
apiRoutes,
) )
val globalFilters = errorFilter return errorFilter
.then(InitialiseRequestContext(contexts)) .then(InitialiseRequestContext(contexts))
.then(SecurityFilter) .then(SecurityFilter)
.then(GZip()) .then(GZip())
.then(routes)
return globalFilters.then(routes)
} }
private inline infix fun PathMethod.public(crossinline handler: PublicHandler) =
this to { handler(it, it.jwtPayload(contexts)) }
private inline infix fun PathMethod.protected(crossinline handler: ProtectedHandler) =
this to { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.transactional(crossinline handler: ProtectedHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)!!) }
private inline infix fun PathMethod.`public transactional`(crossinline handler: PublicHandler) =
this to transactionFilter.then { handler(it, it.jwtPayload(contexts)) }
} }
private typealias PublicHandler = (Request, JwtPayload?) -> Response
private typealias ProtectedHandler = (Request, JwtPayload) -> Response
@@ -0,0 +1,44 @@
package be.simplenotes.app.routes
import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Filter
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 javax.inject.Named
import javax.inject.Singleton
@Singleton
class SettingsRoutes(
private val settingsController: SettingsController,
private val transaction: TransactionFilter,
private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
fun Filter.then(next: ProtectedHandler) = then { req: Request ->
next(req, authLens(req))
}
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 transaction.then(settingsController::settings),
"/export" bind POST to settingsController::export,
)
)
}
}
@@ -10,7 +10,7 @@ import java.util.*
internal class UuidSerializer : KSerializer<UUID> { internal class UuidSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING) get() = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: UUID) { override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString()) encoder.encodeString(value.toString())
@@ -1,10 +0,0 @@
package be.simplenotes.app.utils
import org.ocpsoft.prettytime.PrettyTime
import java.time.LocalDateTime
import java.time.ZoneId
import java.util.*
private val prettyTime = PrettyTime()
fun LocalDateTime.toTimeAgo(): String = prettyTime.format(Date.from(atZone(ZoneId.systemDefault()).toInstant()))
@@ -3,11 +3,13 @@ package be.simplenotes.app.utils
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import javax.inject.Singleton
interface StaticFileResolver { interface StaticFileResolver {
fun resolve(name: String): String? fun resolve(name: String): String?
} }
@Singleton
class StaticFileResolverImpl(json: Json) : StaticFileResolver { class StaticFileResolverImpl(json: Json) : StaticFileResolver {
private val mappings: Map<String, String> private val mappings: Map<String, String>
@@ -13,4 +13,5 @@
<logger name="me.liuwj.ktorm.database" level="INFO"/> <logger name="me.liuwj.ktorm.database" level="INFO"/>
<logger name="com.zaxxer.hikari" level="INFO"/> <logger name="com.zaxxer.hikari" level="INFO"/>
<logger name="org.flywaydb.core" level="INFO"/> <logger name="org.flywaydb.core" level="INFO"/>
<logger name="io.micronaut" level="INFO"/>
</configuration> </configuration>
@@ -1,15 +1,23 @@
package be.simplenotes.app.filters package be.simplenotes.app.filters
import be.simplenotes.domain.security.JwtPayload import be.simplenotes.app.filters.auth.OptionalAuthFilter
import be.simplenotes.domain.security.JwtPayloadExtractor import be.simplenotes.app.filters.auth.OptionalAuthLens
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.types.LoggedInUser
import com.natpryce.hamkrest.assertion.assertThat 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.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.FOUND
import org.http4k.core.Status.Companion.OK import org.http4k.core.Status.Companion.OK
import org.http4k.core.cookie.cookie import org.http4k.core.cookie.cookie
import org.http4k.core.then
import org.http4k.filter.ServerFilters import org.http4k.filter.ServerFilters
import org.http4k.hamkrest.hasBody import org.http4k.hamkrest.hasBody
import org.http4k.hamkrest.hasHeader import org.http4k.hamkrest.hasHeader
@@ -20,22 +28,36 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test import org.junit.jupiter.api.Test
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
internal class AuthFilterTest { internal class RequiredAuthFilterTest {
// region setup // region setup
private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS) private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS)
private val simpleJwt = SimpleJwt(jwtConfig) 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 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( private val app = ServerFilters.InitialiseRequestContext(ctx).then(
routes( routes(
"/optional" bind GET to optionalAuth.then(echoJwtPayloadHandler), "/optional" bind GET to optionalAuth.then { request: Request ->
"/protected" bind GET to requiredAuth.then(echoJwtPayloadHandler) Response(OK).body(optionalLens(request).toString())
},
"/protected" bind GET to requiredAuth.then { request: Request ->
Response(OK).body(requiredLens(request).toString())
}
) )
) )
// endregion // endregion
@@ -58,7 +80,7 @@ internal class AuthFilterTest {
@Test @Test
fun `it should allow a valid token`() { fun `it should allow a valid token`() {
val jwtPayload = JwtPayload(1, "user") val jwtPayload = LoggedInUser(1, "user")
val token = simpleJwt.sign(jwtPayload) val token = simpleJwt.sign(jwtPayload)
val response = app(Request(GET, "/optional").cookie("Bearer", token)) val response = app(Request(GET, "/optional").cookie("Bearer", token))
assertThat(response, hasStatus(OK)) assertThat(response, hasStatus(OK))
@@ -84,7 +106,7 @@ internal class AuthFilterTest {
@Test @Test
fun `it should allow a valid token"`() { fun `it should allow a valid token"`() {
val jwtPayload = JwtPayload(1, "user") val jwtPayload = LoggedInUser(1, "user")
val token = simpleJwt.sign(jwtPayload) val token = simpleJwt.sign(jwtPayload)
val response = app(Request(GET, "/protected").cookie("Bearer", token)) val response = app(Request(GET, "/protected").cookie("Bearer", token))
assertThat(response, hasStatus(OK)) assertThat(response, hasStatus(OK))
@@ -30,7 +30,9 @@ internal class SearchTermsParserKtTest {
createResult("tag:'example' title:'other' end", title = "other", tag = "example", all = "end"), createResult("tag:'example' title:'other' end", title = "other", tag = "example", all = "end"),
createResult( createResult(
"tag:'example abc' title:'other with words' this is the end ", "tag:'example abc' title:'other with words' this is the end ",
title = "other with words", tag = "example abc", all = "this is the end" title = "other with words",
tag = "example abc",
all = "this is the end"
), ),
) )
+11
View File
@@ -0,0 +1,11 @@
import be.simplenotes.Libs
plugins {
id("be.simplenotes.base")
kotlin("kapt")
}
dependencies {
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
}
-15
View File
@@ -1,15 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simplenotes-parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-config</artifactId>
</project>
@@ -1,12 +1,11 @@
package be.simplenotes.app package be.simplenotes.config
import be.simplenotes.config.DataSourceConfig
import be.simplenotes.config.JwtConfig
import be.simplenotes.config.ServerConfig
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton
class Config { @Singleton
class ConfigLoader {
//region Config loading //region Config loading
private val properties: Properties = javaClass private val properties: Properties = javaClass
.getResource("/application.properties") .getResource("/application.properties")
@@ -0,0 +1,17 @@
package be.simplenotes.config
import io.micronaut.context.annotation.Factory
import javax.inject.Singleton
@Factory
class ConfigModule {
@Singleton
internal fun dataSourceConfig(configLoader: ConfigLoader) = configLoader.dataSourceConfig
@Singleton
internal fun jwtConfig(configLoader: ConfigLoader) = configLoader.jwtConfig
@Singleton
internal fun serverConfig(configLoader: ConfigLoader) = configLoader.serverConfig
}
+32
View File
@@ -0,0 +1,32 @@
import be.simplenotes.Libs
plugins {
id("be.simplenotes.base")
id("be.simplenotes.kotlinx-serialization")
kotlin("kapt")
}
dependencies {
implementation(project(":simplenotes-config"))
implementation(project(":simplenotes-types"))
implementation(project(":simplenotes-persistance"))
implementation(project(":simplenotes-search"))
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
implementation(Libs.kotlinxSerializationJson)
implementation(Libs.arrowCoreData)
implementation(Libs.konform)
implementation(Libs.jbcrypt)
implementation(Libs.javaJwt)
implementation(Libs.flexmark)
implementation(Libs.flexmarkGfmTasklist)
implementation(Libs.snakeyaml)
implementation(Libs.owaspHtmlSanitizer)
implementation(Libs.commonsCompress)
testImplementation(Libs.hamkrest)
testImplementation(Libs.junit)
testImplementation(Libs.mockk)
}
-113
View File
@@ -1,113 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simplenotes-parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-domain</artifactId>
<dependencies>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-test-resources</artifactId>
<version>1.0-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.mockk</groupId>
<artifactId>mockk</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.konform</groupId>
<artifactId>konform-jvm</artifactId>
<version>0.2.0</version>
</dependency>
<dependency>
<groupId>org.mindrot</groupId>
<artifactId>jbcrypt</artifactId>
<version>0.4</version>
</dependency>
<dependency>
<groupId>com.auth0</groupId>
<artifactId>java-jwt</artifactId>
<version>3.10.3</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark</artifactId>
<version>0.62.2</version>
</dependency>
<dependency>
<groupId>com.vladsch.flexmark</groupId>
<artifactId>flexmark-ext-gfm-tasklist</artifactId>
<version>0.62.2</version>
</dependency>
<dependency>
<groupId>org.yaml</groupId>
<artifactId>snakeyaml</artifactId>
<version>1.26</version>
</dependency>
<dependency>
<groupId>com.googlecode.owasp-java-html-sanitizer</groupId>
<artifactId>owasp-java-html-sanitizer</artifactId>
<version>20200713.1</version>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-types</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-persistance</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-search</artifactId>
<version>1.0-SNAPSHOT</version>
<scope>compile</scope>
</dependency>
</dependencies>
</project>
@@ -1,37 +0,0 @@
package be.simplenotes.domain
import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.export.ExportUseCase
import be.simplenotes.domain.usecases.export.ExportUseCaseImpl
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownConverterImpl
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
import be.simplenotes.domain.usecases.users.delete.DeleteUseCaseImpl
import be.simplenotes.domain.usecases.users.login.LoginUseCase
import be.simplenotes.domain.usecases.users.login.LoginUseCaseImpl
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
import be.simplenotes.domain.usecases.users.register.RegisterUseCaseImpl
import org.koin.dsl.module
val domainModule = module {
single<LoginUseCase> { LoginUseCaseImpl(get(), get(), get()) }
single<RegisterUseCase> { RegisterUseCaseImpl(get(), get()) }
single<DeleteUseCase> { DeleteUseCaseImpl(get(), get(), get()) }
single { UserService(get(), get(), get(), get()) }
single<PasswordHash> { BcryptPasswordHash() }
single { SimpleJwt(get()) }
single { JwtPayloadExtractor(get()) }
single {
NoteService(get(), get(), get(), get()).apply {
dropAllIndexes()
indexAll()
}
}
single<MarkdownConverter> { MarkdownConverterImpl() }
single<ExportUseCase> { ExportUseCaseImpl(get(), get()) }
}
@@ -1,18 +1,16 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.types.PersistedUser import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.exceptions.JWTVerificationException import com.auth0.jwt.exceptions.JWTVerificationException
import javax.inject.Singleton
data class JwtPayload(val userId: Int, val username: String) { @Singleton
constructor(user: PersistedUser) : this(user.id, user.username)
}
class JwtPayloadExtractor(private val jwt: SimpleJwt) { class JwtPayloadExtractor(private val jwt: SimpleJwt) {
operator fun invoke(token: String): JwtPayload? = try { operator fun invoke(token: String): LoggedInUser? = try {
val decodedJWT = jwt.verifier.verify(token) val decodedJWT = jwt.verifier.verify(token)
val id = decodedJWT.getClaim(userIdField).asInt() ?: null val id = decodedJWT.getClaim(userIdField).asInt() ?: null
val username = decodedJWT.getClaim(usernameField).asString() ?: null val username = decodedJWT.getClaim(usernameField).asString() ?: null
id?.let { username?.let { JwtPayload(id, username) } } id?.let { username?.let { LoggedInUser(id, username) } }
} catch (e: JWTVerificationException) { } catch (e: JWTVerificationException) {
null null
} catch (e: IllegalArgumentException) { } catch (e: IllegalArgumentException) {
@@ -1,13 +1,19 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import org.mindrot.jbcrypt.BCrypt import org.mindrot.jbcrypt.BCrypt
import javax.inject.Inject
import javax.inject.Singleton
internal interface PasswordHash { internal interface PasswordHash {
fun crypt(password: String): String fun crypt(password: String): String
fun verify(password: String, hashedPassword: String): Boolean fun verify(password: String, hashedPassword: String): Boolean
} }
internal class BcryptPasswordHash(test: Boolean = false) : PasswordHash { @Singleton
internal class BcryptPasswordHash constructor(test: Boolean) : PasswordHash {
@Inject
constructor() : this(false)
private val rounds = if (test) 4 else 10 private val rounds = if (test) 4 else 10
override fun crypt(password: String) = BCrypt.hashpw(password, BCrypt.gensalt(rounds))!! override fun crypt(password: String) = BCrypt.hashpw(password, BCrypt.gensalt(rounds))!!
override fun verify(password: String, hashedPassword: String) = BCrypt.checkpw(password, hashedPassword) override fun verify(password: String, hashedPassword: String) = BCrypt.checkpw(password, hashedPassword)
@@ -1,23 +1,26 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton
internal const val userIdField = "i" internal const val userIdField = "i"
internal const val usernameField = "u" internal const val usernameField = "u"
@Singleton
class SimpleJwt(jwtConfig: JwtConfig) { class SimpleJwt(jwtConfig: JwtConfig) {
private val validityInMs = TimeUnit.MILLISECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit) private val validityInMs = TimeUnit.MILLISECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit)
private val algorithm = Algorithm.HMAC256(jwtConfig.secret) private val algorithm = Algorithm.HMAC256(jwtConfig.secret)
val verifier: JWTVerifier = JWT.require(algorithm).build() val verifier: JWTVerifier = JWT.require(algorithm).build()
fun sign(jwtPayload: JwtPayload): String = JWT.create() fun sign(loggedInUser: LoggedInUser): String = JWT.create()
.withClaim(userIdField, jwtPayload.userId) .withClaim(userIdField, loggedInUser.userId)
.withClaim(usernameField, jwtPayload.username) .withClaim(usernameField, loggedInUser.username)
.withExpiresAt(getExpiration()) .withExpiresAt(getExpiration())
.sign(algorithm) .sign(algorithm)
@@ -1,10 +1,6 @@
package be.simplenotes.domain.usecases package be.simplenotes.domain.usecases
import arrow.core.Either import arrow.core.computations.either
import arrow.core.extensions.fx
import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.domain.security.HtmlSanitizer import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.domain.usecases.markdown.MarkdownConverter import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
@@ -12,8 +8,13 @@ import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import be.simplenotes.search.SearchTerms import be.simplenotes.search.SearchTerms
import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
import java.util.* import java.util.*
import javax.inject.Singleton
@Singleton
class NoteService( class NoteService(
private val markdownConverter: MarkdownConverter, private val markdownConverter: MarkdownConverter,
private val noteRepository: NoteRepository, private val noteRepository: NoteRepository,
@@ -21,7 +22,7 @@ class NoteService(
private val searcher: NoteSearcher, private val searcher: NoteSearcher,
) { ) {
fun create(userId: Int, markdownText: String) = Either.fx<MarkdownParsingError, PersistedNote> { fun create(userId: Int, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> {
val persistedNote = !markdownConverter.renderDocument(markdownText) val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) } .map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { Note(it.metadata, markdown = markdownText, html = it.html) }
@@ -31,7 +32,7 @@ class NoteService(
persistedNote persistedNote
} }
fun update(userId: Int, uuid: UUID, markdownText: String) = Either.fx<MarkdownParsingError, PersistedNote?> { fun update(userId: Int, uuid: UUID, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote?> {
val persistedNote = !markdownConverter.renderDocument(markdownText) val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) } .map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) } .map { Note(it.metadata, markdown = markdownText, html = it.html) }
@@ -4,7 +4,9 @@ import be.simplenotes.domain.usecases.export.ExportUseCase
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
import be.simplenotes.domain.usecases.users.login.LoginUseCase import be.simplenotes.domain.usecases.users.login.LoginUseCase
import be.simplenotes.domain.usecases.users.register.RegisterUseCase import be.simplenotes.domain.usecases.users.register.RegisterUseCase
import javax.inject.Singleton
@Singleton
class UserService( class UserService(
loginUseCase: LoginUseCase, loginUseCase: LoginUseCase,
registerUseCase: RegisterUseCase, registerUseCase: RegisterUseCase,
@@ -1,7 +1,8 @@
package be.simplenotes.domain.usecases.export package be.simplenotes.domain.usecases.export
import be.simplenotes.types.ExportedNote
import be.simplenotes.persistance.repositories.NoteRepository import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.types.ExportedNote
import io.micronaut.context.annotation.Primary
import kotlinx.serialization.builtins.ListSerializer import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry import org.apache.commons.compress.archivers.zip.ZipArchiveEntry
@@ -9,8 +10,14 @@ import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.ByteArrayOutputStream import java.io.ByteArrayOutputStream
import java.io.InputStream import java.io.InputStream
import javax.inject.Singleton
internal class ExportUseCaseImpl(private val noteRepository: NoteRepository, private val json: Json) : ExportUseCase { @Primary
@Singleton
internal class ExportUseCaseImpl(
private val noteRepository: NoteRepository,
private val json: Json,
) : ExportUseCase {
override fun exportAsJson(userId: Int): String { override fun exportAsJson(userId: Int): String {
val notes = noteRepository.export(userId) val notes = noteRepository.export(userId)
return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes) return json.encodeToString(ListSerializer(ExportedNote.serializer()), notes)
@@ -31,7 +38,6 @@ internal class ExportUseCaseImpl(private val noteRepository: NoteRepository, pri
} }
} }
class ZipOutput : AutoCloseable { class ZipOutput : AutoCloseable {
val outputStream = ByteArrayOutputStream() val outputStream = ByteArrayOutputStream()
private val zipOutputStream = ZipArchiveOutputStream(outputStream) private val zipOutputStream = ZipArchiveOutputStream(outputStream)
@@ -1,11 +1,11 @@
package be.simplenotes.domain.usecases.markdown package be.simplenotes.domain.usecases.markdown
import arrow.core.Either import arrow.core.Either
import arrow.core.extensions.fx import arrow.core.computations.either
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import be.simplenotes.types.NoteMetadata
import be.simplenotes.domain.validation.NoteValidations import be.simplenotes.domain.validation.NoteValidations
import be.simplenotes.types.NoteMetadata
import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension import com.vladsch.flexmark.ext.gfm.tasklist.TaskListExtension
import com.vladsch.flexmark.html.HtmlRenderer import com.vladsch.flexmark.html.HtmlRenderer
import com.vladsch.flexmark.parser.Parser import com.vladsch.flexmark.parser.Parser
@@ -14,6 +14,7 @@ import io.konform.validation.ValidationErrors
import org.yaml.snakeyaml.Yaml import org.yaml.snakeyaml.Yaml
import org.yaml.snakeyaml.parser.ParserException import org.yaml.snakeyaml.parser.ParserException
import org.yaml.snakeyaml.scanner.ScannerException import org.yaml.snakeyaml.scanner.ScannerException
import javax.inject.Singleton
sealed class MarkdownParsingError sealed class MarkdownParsingError
object MissingMeta : MarkdownParsingError() object MissingMeta : MarkdownParsingError()
@@ -28,6 +29,7 @@ interface MarkdownConverter {
fun renderDocument(input: String): Either<MarkdownParsingError, Document> fun renderDocument(input: String): Either<MarkdownParsingError, Document>
} }
@Singleton
internal class MarkdownConverterImpl : MarkdownConverter { internal class MarkdownConverterImpl : MarkdownConverter {
private val yamlBoundPattern = "-{3}".toRegex() private val yamlBoundPattern = "-{3}".toRegex()
private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> { private fun splitMetaFromDocument(input: String): Either<MissingMeta, MetaMdPair> {
@@ -73,10 +75,10 @@ internal class MarkdownConverterImpl : MarkdownConverter {
private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render) private fun renderMarkdown(markdown: String) = parser.parse(markdown).run(renderer::render)
override fun renderDocument(input: String) = Either.fx<MarkdownParsingError, Document> { override fun renderDocument(input: String) = either.eager<MarkdownParsingError, Document> {
val (meta, md) = !splitMetaFromDocument(input) val (meta, md) = !splitMetaFromDocument(input)
val parsedMeta = !parseMeta(meta) val parsedMeta = !parseMeta(meta)
!NoteValidations.validateMetadata(parsedMeta).toEither { }.swap() !Either.fromNullable(NoteValidations.validateMetadata(parsedMeta)).swap()
val html = renderMarkdown(md) val html = renderMarkdown(md)
Document(parsedMeta, html) Document(parsedMeta, html)
} }
@@ -1,27 +1,31 @@
package be.simplenotes.domain.usecases.users.delete package be.simplenotes.domain.usecases.users.delete
import arrow.core.Either import arrow.core.Either
import arrow.core.extensions.fx import arrow.core.computations.either
import arrow.core.rightIfNotNull import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.search.NoteSearcher import be.simplenotes.search.NoteSearcher
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Primary
@Singleton
internal class DeleteUseCaseImpl( internal class DeleteUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash, private val passwordHash: PasswordHash,
private val searcher: NoteSearcher, private val searcher: NoteSearcher,
) : DeleteUseCase { ) : DeleteUseCase {
override fun delete(form: DeleteForm) = Either.fx<DeleteError, Unit> { override fun delete(form: DeleteForm) = either.eager<DeleteError, Unit> {
val user = !UserValidations.validateDelete(form) val user = !UserValidations.validateDelete(form)
val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered } val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered }
!Either.cond( !Either.conditionally(
passwordHash.verify(user.password, persistedUser.password), passwordHash.verify(user.password, persistedUser.password),
{ Unit }, { DeleteError.WrongPassword },
{ DeleteError.WrongPassword } { Unit }
) )
!Either.cond(userRepository.delete(persistedUser.id), { Unit }, { DeleteError.Unregistered }) !Either.conditionally(userRepository.delete(persistedUser.id), { DeleteError.Unregistered }, { Unit })
searcher.dropIndex(persistedUser.id) searcher.dropIndex(persistedUser.id)
} }
} }
@@ -1,25 +1,28 @@
package be.simplenotes.domain.usecases.users.login package be.simplenotes.domain.usecases.users.login
import arrow.core.Either import arrow.core.computations.either
import arrow.core.extensions.fx
import arrow.core.filterOrElse import arrow.core.filterOrElse
import arrow.core.rightIfNotNull import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.types.LoggedInUser
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Singleton
@Primary
internal class LoginUseCaseImpl( internal class LoginUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash, private val passwordHash: PasswordHash,
private val jwt: SimpleJwt private val jwt: SimpleJwt
) : LoginUseCase { ) : LoginUseCase {
override fun login(form: LoginForm) = Either.fx<LoginError, Token> { override fun login(form: LoginForm) = either.eager<LoginError, Token> {
val user = !UserValidations.validateLogin(form) val user = !UserValidations.validateLogin(form)
!userRepository.find(user.username) !userRepository.find(user.username)
.rightIfNotNull { Unregistered } .rightIfNotNull { Unregistered }
.filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword }) .filterOrElse({ passwordHash.verify(form.password!!, it.password) }, { WrongPassword })
.map { jwt.sign(JwtPayload(it)) } .map { jwt.sign(LoggedInUser(it)) }
} }
} }
@@ -3,11 +3,15 @@ package be.simplenotes.domain.usecases.users.register
import arrow.core.Either import arrow.core.Either
import arrow.core.filterOrElse import arrow.core.filterOrElse
import arrow.core.leftIfNull import arrow.core.leftIfNull
import be.simplenotes.types.PersistedUser
import be.simplenotes.domain.security.PasswordHash import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.validation.UserValidations import be.simplenotes.domain.validation.UserValidations
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.types.PersistedUser
import io.micronaut.context.annotation.Primary
import javax.inject.Singleton
@Primary
@Singleton
internal class RegisterUseCaseImpl( internal class RegisterUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash private val passwordHash: PasswordHash
@@ -1,8 +1,8 @@
package be.simplenotes.domain.usecases.users.register package be.simplenotes.domain.usecases.users.register
import arrow.core.Either import arrow.core.Either
import be.simplenotes.types.PersistedUser
import be.simplenotes.domain.usecases.users.login.LoginForm import be.simplenotes.domain.usecases.users.login.LoginForm
import be.simplenotes.types.PersistedUser
import io.konform.validation.ValidationErrors import io.konform.validation.ValidationErrors
sealed class RegisterError sealed class RegisterError
@@ -1,8 +1,7 @@
package be.simplenotes.domain.validation package be.simplenotes.domain.validation
import arrow.core.*
import be.simplenotes.types.NoteMetadata
import be.simplenotes.domain.usecases.markdown.ValidationError import be.simplenotes.domain.usecases.markdown.ValidationError
import be.simplenotes.types.NoteMetadata
import io.konform.validation.Validation import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxItems import io.konform.validation.jsonschema.maxItems
import io.konform.validation.jsonschema.maxLength import io.konform.validation.jsonschema.maxLength
@@ -28,9 +27,9 @@ internal object NoteValidations {
} }
} }
fun validateMetadata(meta: NoteMetadata): Option<ValidationError> { fun validateMetadata(meta: NoteMetadata): ValidationError? {
val errors = metaValidator.validate(meta).errors val errors = metaValidator.validate(meta).errors
return if (errors.isEmpty()) none() return if (errors.isEmpty()) null
else return ValidationError(errors).some() else return ValidationError(errors)
} }
} }
@@ -3,13 +3,13 @@ package be.simplenotes.domain.validation
import arrow.core.Either import arrow.core.Either
import arrow.core.left import arrow.core.left
import arrow.core.right import arrow.core.right
import be.simplenotes.types.User
import be.simplenotes.domain.usecases.users.delete.DeleteError import be.simplenotes.domain.usecases.users.delete.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm import be.simplenotes.domain.usecases.users.delete.DeleteForm
import be.simplenotes.domain.usecases.users.login.InvalidLoginForm import be.simplenotes.domain.usecases.users.login.InvalidLoginForm
import be.simplenotes.domain.usecases.users.login.LoginForm import be.simplenotes.domain.usecases.users.login.LoginForm
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
import be.simplenotes.domain.usecases.users.register.RegisterForm import be.simplenotes.domain.usecases.users.register.RegisterForm
import be.simplenotes.types.User
import io.konform.validation.Validation import io.konform.validation.Validation
import io.konform.validation.jsonschema.maxLength import io.konform.validation.jsonschema.maxLength
import io.konform.validation.jsonschema.minLength import io.konform.validation.jsonschema.minLength
@@ -1,7 +1,8 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.domain.usecases.users.login.Token
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.usecases.users.login.Token
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import com.natpryce.hamkrest.absent import com.natpryce.hamkrest.absent
@@ -13,7 +14,7 @@ import org.junit.jupiter.params.provider.MethodSource
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.stream.Stream import java.util.stream.Stream
internal class JwtPayloadExtractorTest { internal class LoggedInUserExtractorTest {
private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS) private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
private val simpleJwt = SimpleJwt(jwtConfig) private val simpleJwt = SimpleJwt(jwtConfig)
private val jwtPayloadExtractor = JwtPayloadExtractor(simpleJwt) private val jwtPayloadExtractor = JwtPayloadExtractor(simpleJwt)
@@ -45,6 +46,6 @@ internal class JwtPayloadExtractorTest {
@Test @Test
fun `parse valid token`() { fun `parse valid token`() {
val token = createToken(username = "someone", id = 1) val token = createToken(username = "someone", id = 1)
assertThat(jwtPayloadExtractor(token), equalTo(JwtPayload(1, "someone"))) assertThat(jwtPayloadExtractor(token), equalTo(LoggedInUser(1, "someone")))
} }
} }
@@ -1,12 +1,12 @@
package be.simplenotes.domain.usecases.users.login package be.simplenotes.domain.usecases.users.login
import be.simplenotes.types.PersistedUser import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.BcryptPasswordHash import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.testutils.isLeftOfType import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight import be.simplenotes.domain.testutils.isRight
import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.types.PersistedUser
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import io.mockk.* import io.mockk.*
import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.BeforeEach
@@ -1,10 +1,10 @@
package be.simplenotes.domain.usecases.users.register package be.simplenotes.domain.usecases.users.register
import be.simplenotes.types.PersistedUser
import be.simplenotes.domain.security.BcryptPasswordHash import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.testutils.isLeftOfType import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight import be.simplenotes.domain.testutils.isRight
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.types.PersistedUser
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import com.natpryce.hamkrest.equalTo import com.natpryce.hamkrest.equalTo
import io.mockk.* import io.mockk.*
+52
View File
@@ -0,0 +1,52 @@
import be.simplenotes.Libs
plugins {
id("be.simplenotes.base")
kotlin("kapt")
`java-test-fixtures`
}
dependencies {
implementation(project(":simplenotes-types"))
implementation(project(":simplenotes-config"))
implementation(Libs.mariadbClient)
implementation(Libs.h2)
implementation(Libs.flywayCore)
implementation(Libs.hikariCP)
implementation(Libs.ktormCore)
implementation(Libs.ktormMysql)
implementation(Libs.mapstruct)
kapt(Libs.mapstructProcessor)
implementation(Libs.micronaut)
kapt(Libs.micronautProcessor)
testImplementation(Libs.micronaut)
kaptTest(Libs.micronautProcessor)
testImplementation(Libs.junit)
testImplementation(Libs.assertJ)
testImplementation(Libs.logbackClassic)
testImplementation(Libs.mariaTestContainer)
testFixturesImplementation(project(":simplenotes-types"))
testFixturesImplementation(project(":simplenotes-config"))
testFixturesImplementation(project(":simplenotes-persistance"))
testFixturesImplementation(Libs.micronaut)
kaptTestFixtures(Libs.micronautProcessor)
testFixturesImplementation(Libs.faker) {
exclude(group = "org.yaml")
}
testFixturesImplementation(Libs.snakeyaml)
testFixturesImplementation(Libs.mariaTestContainer)
testFixturesImplementation(Libs.flywayCore)
testFixturesImplementation(Libs.junit)
testFixturesImplementation(Libs.ktormCore)
testFixturesImplementation(Libs.hikariCP)
}
-84
View File
@@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>simplenotes-parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>simplenotes-persistance</artifactId>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-types</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-config</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>simplenotes-test-resources</artifactId>
<version>1.0-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-core</artifactId>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.mariadb.jdbc</groupId>
<artifactId>mariadb-java-client</artifactId>
<version>2.6.2</version>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<version>1.4.200</version>
</dependency>
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>6.5.4</version>
</dependency>
<dependency>
<groupId>com.zaxxer</groupId>
<artifactId>HikariCP</artifactId>
<version>3.4.3</version>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-core</artifactId>
</dependency>
<dependency>
<groupId>me.liuwj.ktorm</groupId>
<artifactId>ktorm-support-mysql</artifactId>
</dependency>
</dependencies>
</project>
@@ -1,16 +1,19 @@
package be.simplenotes.persistance package be.simplenotes.persistance
import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistance.utils.DbType import be.simplenotes.persistance.utils.DbType
import be.simplenotes.persistance.utils.type import be.simplenotes.persistance.utils.type
import be.simplenotes.config.DataSourceConfig
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import me.liuwj.ktorm.database.asIterable import me.liuwj.ktorm.database.asIterable
import me.liuwj.ktorm.database.use
import java.sql.SQLTransientException import java.sql.SQLTransientException
import javax.inject.Singleton
interface DbHealthCheck { interface DbHealthCheck {
fun isOk(): Boolean fun isOk(): Boolean
} }
@Singleton
internal class DbHealthCheckImpl( internal class DbHealthCheckImpl(
private val db: Database, private val db: Database,
private val dataSourceConfig: DataSourceConfig, private val dataSourceConfig: DataSourceConfig,
@@ -1,15 +1,17 @@
package be.simplenotes.persistance package be.simplenotes.persistance
import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistance.utils.DbType import be.simplenotes.persistance.utils.DbType
import be.simplenotes.persistance.utils.type import be.simplenotes.persistance.utils.type
import be.simplenotes.config.DataSourceConfig
import org.flywaydb.core.Flyway import org.flywaydb.core.Flyway
import javax.inject.Singleton
import javax.sql.DataSource import javax.sql.DataSource
interface DbMigrations { interface DbMigrations {
fun migrate() fun migrate()
} }
@Singleton
internal class DbMigrationsImpl( internal class DbMigrationsImpl(
private val dataSource: DataSource, private val dataSource: DataSource,
private val dataSourceConfig: DataSourceConfig, private val dataSourceConfig: DataSourceConfig,
@@ -2,46 +2,42 @@ package be.simplenotes.persistance
import be.simplenotes.config.DataSourceConfig import be.simplenotes.config.DataSourceConfig
import be.simplenotes.persistance.converters.NoteConverter import be.simplenotes.persistance.converters.NoteConverter
import be.simplenotes.persistance.converters.NoteConverterImpl
import be.simplenotes.persistance.converters.UserConverter import be.simplenotes.persistance.converters.UserConverter
import be.simplenotes.persistance.converters.UserConverterImpl
import be.simplenotes.persistance.notes.NoteRepositoryImpl
import be.simplenotes.persistance.repositories.NoteRepository
import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.persistance.users.UserRepositoryImpl
import com.zaxxer.hikari.HikariConfig import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource import com.zaxxer.hikari.HikariDataSource
import io.micronaut.context.annotation.Bean
import io.micronaut.context.annotation.Factory
import me.liuwj.ktorm.database.Database import me.liuwj.ktorm.database.Database
import org.koin.dsl.bind import org.mapstruct.factory.Mappers
import org.koin.dsl.module import javax.inject.Singleton
import org.koin.dsl.onClose
import javax.sql.DataSource import javax.sql.DataSource
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource { @Factory
val hikariConfig = HikariConfig().also { class PersistanceModule {
it.jdbcUrl = conf.jdbcUrl
it.driverClassName = conf.driverClassName
it.username = conf.username
it.password = conf.password
it.maximumPoolSize = conf.maximumPoolSize
it.connectionTimeout = conf.connectionTimeout
}
return HikariDataSource(hikariConfig)
}
val migrationModule = module { @Singleton
single<DbMigrations> { DbMigrationsImpl(get(), get()) } internal fun noteConverter() = Mappers.getMapper(NoteConverter::class.java)
}
val persistanceModule = module { @Singleton
single<NoteConverter> { NoteConverterImpl() } internal fun userConverter() = Mappers.getMapper(UserConverter::class.java)
single<UserConverter> { UserConverterImpl() }
single<UserRepository> { UserRepositoryImpl(get(), get()) } @Singleton
single<NoteRepository> { NoteRepositoryImpl(get(), get()) } internal fun database(migrations: DbMigrations, dataSource: DataSource): Database {
single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() } migrations.migrate()
single { return Database.connect(dataSource)
get<DbMigrations>().migrate() }
Database.connect(get<DataSource>())
@Singleton
@Bean(preDestroy = "close")
internal fun dataSource(conf: DataSourceConfig): HikariDataSource {
val hikariConfig = HikariConfig().also {
it.jdbcUrl = conf.jdbcUrl
it.driverClassName = conf.driverClassName
it.username = conf.username
it.password = conf.password
it.maximumPoolSize = conf.maximumPoolSize
it.connectionTimeout = conf.connectionTimeout
}
return HikariDataSource(hikariConfig)
} }
single<DbHealthCheck> { DbHealthCheckImpl(get(), get()) }
} }
@@ -1,20 +1,20 @@
package be.simplenotes.persistance.converters package be.simplenotes.persistance.converters
import be.simplenotes.types.*
import be.simplenotes.persistance.notes.NoteEntity import be.simplenotes.persistance.notes.NoteEntity
import be.simplenotes.types.*
import me.liuwj.ktorm.entity.Entity import me.liuwj.ktorm.entity.Entity
import org.mapstruct.Mapper import org.mapstruct.Mapper
import org.mapstruct.Mapping import org.mapstruct.Mapping
import org.mapstruct.Mappings import org.mapstruct.Mappings
import org.mapstruct.ReportingPolicy
import java.time.LocalDateTime import java.time.LocalDateTime
import java.util.* import java.util.*
/** /**
* This is an abstract class because kotlin default methods in interface are not seen as default in kapt * This is an abstract class because kotlin default methods in interface are not seen as default in kapt
* @see [KT-25960](https://youtrack.jetbrains.com/issue/KT-25960) * @see [KT-25960](https://youtrack.jetbrains.com/issue/KT-25960)
*/ */
@Mapper(uses = [NoteEntityFactory::class, UserEntityFactory::class]) @Mapper(uses = [NoteEntityFactory::class, UserEntityFactory::class], unmappedTargetPolicy = ReportingPolicy.IGNORE)
internal abstract class NoteConverter { internal abstract class NoteConverter {
fun toNote(entity: NoteEntity, tags: Tags) = fun toNote(entity: NoteEntity, tags: Tags) =
@@ -34,7 +34,11 @@ internal abstract class NoteConverter {
fun toPersistedNote(entity: NoteEntity, tags: Tags) = PersistedNote( fun toPersistedNote(entity: NoteEntity, tags: Tags) = PersistedNote(
NoteMetadata(title = entity.title, tags = tags), NoteMetadata(title = entity.title, tags = tags),
entity.markdown, entity.html, entity.updatedAt, entity.uuid, entity.public entity.markdown,
entity.html,
entity.updatedAt,
entity.uuid,
entity.public
) )
@Mappings( @Mappings(
@@ -45,15 +49,15 @@ internal abstract class NoteConverter {
abstract fun toExportedNote(entity: NoteEntity, tags: Tags): ExportedNote abstract fun toExportedNote(entity: NoteEntity, tags: Tags): ExportedNote
fun toEntity(note: Note, uuid: UUID, userId: Int, updatedAt: LocalDateTime) = NoteEntity { fun toEntity(note: Note, uuid: UUID, userId: Int, updatedAt: LocalDateTime) = NoteEntity {
this.title = note.meta.title this.title = note.meta.title
this.markdown = note.markdown this.markdown = note.markdown
this.html = note.html this.html = note.html
this.uuid = uuid this.uuid = uuid
this.deleted = false this.deleted = false
this.public = false this.public = false
this.user.id = userId this.user.id = userId
this.updatedAt = updatedAt this.updatedAt = updatedAt
} }
@Mappings( @Mappings(
Mapping(target = ".", source = "note"), Mapping(target = ".", source = "note"),
@@ -72,7 +76,6 @@ internal abstract class NoteConverter {
@Mapping(target = "deleted", source = "trash") @Mapping(target = "deleted", source = "trash")
abstract fun toEntity(exportedNote: ExportedNote): NoteEntity abstract fun toEntity(exportedNote: ExportedNote): NoteEntity
} }
typealias Tags = List<String> typealias Tags = List<String>
@@ -1,12 +1,13 @@
package be.simplenotes.persistance.converters package be.simplenotes.persistance.converters
import be.simplenotes.persistance.users.UserEntity
import be.simplenotes.types.PersistedUser import be.simplenotes.types.PersistedUser
import be.simplenotes.types.User import be.simplenotes.types.User
import be.simplenotes.persistance.users.UserEntity
import me.liuwj.ktorm.entity.Entity import me.liuwj.ktorm.entity.Entity
import org.mapstruct.Mapper import org.mapstruct.Mapper
import org.mapstruct.ReportingPolicy
@Mapper(uses = [UserEntityFactory::class]) @Mapper(uses = [UserEntityFactory::class], unmappedTargetPolicy = ReportingPolicy.IGNORE)
internal interface UserConverter { internal interface UserConverter {
fun toUser(userEntity: UserEntity): User fun toUser(userEntity: UserEntity): User
fun toPersistedUser(userEntity: UserEntity): PersistedUser fun toPersistedUser(userEntity: UserEntity): PersistedUser

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