40 Commits

Author SHA1 Message Date
hubert e6a7af840a Fix db health check 2020-11-06 23:44:57 +01:00
hubert 568a2c6831 Move search parsing to domain layer 2020-11-06 23:44:37 +01:00
hubert c39a20cf96 Move health check to domain layer 2020-11-05 14:44:59 +01:00
hubert bf56314473 Move transactions to domain layer 2020-11-05 14:37:20 +01:00
hubert 11caff1634 Format 2020-11-03 18:36:09 +01:00
hubert b1478fd154 Why ?! 2020-11-03 18:36:09 +01:00
hubert dd174a6327 Log discarded html tags + small refactor 2020-11-03 18:36:09 +01:00
hubert 941380ad16 Load configuration with micronaut 2020-11-03 18:36:09 +01:00
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
hubert e5a2b8993f Remove module-infos
Some libraires are not yet ready..

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

HikariCP has an invalid module-info.java

Kapt doesn't work
2020-10-23 23:20:58 +02:00
hubert 38750a588c Move packages + remove circular dependencies 2020-10-23 23:20:58 +02:00
hubert ee026ec829 Move junit config to simplenotes-test-resources 2020-10-23 16:36:44 +02:00
hubert 29b024d360 Move ArrowAssertions to simplenotes-domain 2020-10-23 16:30:43 +02:00
hubert cd12d1561a Move config into simplenotes-config module 2020-10-23 16:24:50 +02:00
hubert c2eaf3d0cc Move types into simplenotes-types module 2020-10-23 16:12:40 +02:00
hubert 4c9ac8944e Prefix maven modules 2020-10-23 15:45:28 +02:00
hubert 4ff97044f0 Change public/private button css 2020-10-23 08:26:03 +02:00
hubert ead1932d48 Fix some css 2020-10-23 07:16:07 +02:00
hubert 4a7dcec363 Use Json lenses 2020-10-23 06:22:44 +02:00
hubert cb76a3253d Use mapstruct 2020-10-21 22:55:36 +02:00
214 changed files with 3063 additions and 2382 deletions
+4 -8
View File
@@ -1,10 +1,6 @@
## can be generated with `openssl rand -base64 32`
JWT_SECRET=
#
## can be generated with `openssl rand -base64 32`
# mariadb
MYSQL_ROOT_PASSWORD=
#
## can be generated with `openssl rand -base64 32`
MYSQL_PASSWORD=
# password should be the same as mysql_password
PASSWORD=
# simplenotes
DB_PASSWORD=
JWT_SECRET=
+5 -97
View File
@@ -1,24 +1,6 @@
# Java
.mtj.tmp/
*.class
*.jar
*.war
*.ear
*.nar
hs_err_pid*
# Maven
target/
pom.xml.tag
pom.xml.releaseBackup
pom.xml.versionsBackup
pom.xml.next
pom.xml.bak
release.properties
dependency-reduced-pom.xml
buildNumber.properties
.mvn/timing.properties
.mvn/wrapper/maven-wrapper.jar
# Gradle
build/
.gradle
# IntelliJ
out/
@@ -28,11 +10,8 @@ out/
*.ipr
*.iws
# Vue
node_modules
/dist
# Local env files
.env
.env.local
.env.*.local
@@ -49,85 +28,13 @@ pids
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
# nyc test coverage
.nyc_output
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# TypeScript v1 declaration files
typings/
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
# parcel-bundler cache (https://parceljs.org/)
.cache
# next.js build output
.next
# nuxt.js build output
.nuxt
# Nuxt generate
dist
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless
# IDE / Editor
.idea
# Service worker
sw.*
*.private.env.json
# Certificates
data/
letsencrypt/
# generated resources
app/src/main/resources/css-manifest.json
app/src/main/resources/static/styles*
# h2 db
*.db
@@ -136,3 +43,4 @@ app/src/main/resources/static/styles*
# python
__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 -24
View File
@@ -1,25 +1,3 @@
FROM maven:3.6.3-jdk-14 as builder
WORKDIR /tmp
# Cache dependencies
COPY pom.xml .
COPY app/pom.xml app/pom.xml
COPY domain/pom.xml domain/pom.xml
COPY persistance/pom.xml persistance/pom.xml
COPY shared/pom.xml shared/pom.xml
COPY search/pom.xml search/pom.xml
RUN mvn verify clean --fail-never
COPY app/src app/src
COPY domain/src domain/src
COPY persistance/src persistance/src
COPY shared/src shared/src
COPY search/src search/src
RUN mvn -Dstyle.color=always package
FROM openjdk:14-alpine as jdkbuilder
RUN apk add --no-cache binutils
@@ -42,8 +20,21 @@ RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER
COPY --from=builder /tmp/app/target/app-*.jar /app/app.jar
COPY --from=jdkbuilder /myjdk /myjdk
COPY simplenotes-app/build/libs/simplenotes-app-with-dependencies*.jar /app/simplenotes.jar
WORKDIR /app
CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms64m", "-Xmx256m", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "app.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" \
]
+1 -2
View File
@@ -15,5 +15,4 @@
## Configuration
The app is configured with environments variables.
If no match is found within the env, a default value is read from a properties file in /app/src/main/resources/application.properties.
Don't use the default values for secrets ! Every value inside *.env.dist* should be changed.
If no match is found within the env, a default value is read from a yaml file in simplenotes-app/src/main/resources/application.yaml.
-191
View File
@@ -1,191 +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>parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>app</artifactId>
<properties>
<http4k.version>3.268.0</http4k.version>
</properties>
<dependencies>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>persistance</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>search</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>domain</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>shared</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>
<exclusions>
<exclusion>
<groupId>org.eclipse.jetty.websocket</groupId>
<artifactId>javax-websocket-server-impl</artifactId>
</exclusion>
</exclusions>
</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>shared</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>
-48
View File
@@ -1,48 +0,0 @@
package be.simplenotes.app
import be.simplenotes.shared.config.DataSourceConfig
import be.simplenotes.shared.config.JwtConfig
import be.simplenotes.shared.config.ServerConfig
import java.util.*
import java.util.concurrent.TimeUnit
class Config {
//region Config loading
private val properties: Properties = javaClass
.getResource("/application.properties")
.openStream()
.use {
Properties().apply { load(it) }
}
private val env = System.getenv()
private fun value(key: String): String =
env[key.toUpperCase().replace(".", "_")]
?: properties.getProperty(key)
?: error("Missing config key $key")
//endregion
val jwtConfig
get() = JwtConfig(
secret = value("jwt.secret"),
validity = value("jwt.validity").toLong(),
timeUnit = TimeUnit.HOURS,
)
val dataSourceConfig
get() = DataSourceConfig(
jdbcUrl = value("jdbcUrl"),
driverClassName = value("driverClassName"),
username = value("username"),
password = value("password"),
maximumPoolSize = value("maximumPoolSize").toInt(),
connectionTimeout = value("connectionTimeout").toLong()
)
val serverConfig
get() = ServerConfig(
host = value("host"),
port = value("port").toInt(),
)
}
-31
View File
@@ -1,31 +0,0 @@
package be.simplenotes.app
import be.simplenotes.app.extensions.addShutdownHook
import be.simplenotes.app.modules.*
import be.simplenotes.domain.domainModule
import be.simplenotes.persistance.migrationModule
import be.simplenotes.persistance.persistanceModule
import be.simplenotes.search.searchModule
import org.koin.core.context.startKoin
import org.koin.core.context.unloadKoinModules
fun main() {
startKoin {
modules(
serverModule,
persistanceModule,
migrationModule,
configModule,
baseModule,
userModule,
noteModule,
settingsModule,
domainModule,
searchModule,
apiModule,
jsonModule
)
}.addShutdownHook()
unloadKoinModules(listOf(migrationModule, configModule))
}
@@ -1,77 +0,0 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.json
import be.simplenotes.app.utils.parseSearchTerms
import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.NoteService
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.builtins.ListSerializer
import kotlinx.serialization.json.Json
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.OK
import org.http4k.routing.path
import java.util.*
class ApiNoteController(private val noteService: NoteService, private val json: Json) {
fun createNote(request: Request, jwtPayload: JwtPayload): Response {
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
return noteService.create(jwtPayload.userId, content).fold(
{
Response(BAD_REQUEST)
},
{
Response(OK).json(json.encodeToString(UuidContent.serializer(), UuidContent(it.uuid)))
}
)
}
fun notes(request: Request, jwtPayload: JwtPayload): Response {
val notes = noteService.paginatedNotes(jwtPayload.userId, page = 1).notes
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
return Response(OK).json(json)
}
fun note(request: Request, jwtPayload: JwtPayload): Response {
val uuid = request.path("uuid")!!
return noteService.find(jwtPayload.userId, UUID.fromString(uuid))
?.let { Response(OK).json(json.encodeToString(PersistedNote.serializer(), it)) }
?: Response(NOT_FOUND)
}
fun update(request: Request, jwtPayload: JwtPayload): Response {
val uuid = UUID.fromString(request.path("uuid")!!)
val content = json.decodeFromString(NoteContent.serializer(), request.bodyString()).content
return noteService.update(jwtPayload.userId, uuid, content).fold({
Response(BAD_REQUEST)
}, {
if (it == null) Response(NOT_FOUND)
else Response(OK)
})
}
fun search(request: Request, jwtPayload: JwtPayload): Response {
val query = json.decodeFromString(SearchContent.serializer(), request.bodyString()).query
val terms = parseSearchTerms(query)
val notes = noteService.search(jwtPayload.userId, terms)
val json = json.encodeToString(ListSerializer(PersistedNoteMetadata.serializer()), notes)
return Response(OK).json(json)
}
}
@Serializable
data class NoteContent(val content: String)
@Serializable
data class UuidContent(@Contextual val uuid: UUID)
@Serializable
data class SearchContent(@Contextual val query: String)
@@ -1,26 +0,0 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.json
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.LoginForm
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status
class ApiUserController(private val userService: UserService, private val json: Json) {
fun login(request: Request): Response {
val form = json.decodeFromString(LoginForm.serializer(), request.bodyString())
val result = userService.login(form)
return result.fold({
Response(Status.BAD_REQUEST)
}, {
Response(Status.OK).json(json.encodeToString(Token.serializer(), Token(it)))
})
}
}
@Serializable
data class Token(val token: String)
@@ -1,13 +0,0 @@
package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.BaseView
import be.simplenotes.domain.security.JwtPayload
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
class BaseController(private val view: BaseView) {
fun index(@Suppress("UNUSED_PARAMETER") request: Request, jwtPayload: JwtPayload?) =
Response(OK).html(view.renderHome(jwtPayload))
}
@@ -1,12 +0,0 @@
package be.simplenotes.app.controllers
import be.simplenotes.persistance.DbHealthCheck
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
class HealthCheckController(private val dbHealthCheck: DbHealthCheck) {
fun healthCheck(request: Request) =
if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
}
@@ -1,17 +0,0 @@
package be.simplenotes.app.extensions
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.FOUND
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
fun Response.html(html: String) = body(html)
.header("Content-Type", "text/html; charset=utf-8")
.header("Cache-Control", "no-cache")
fun Response.json(json: String) = body(json).header("Content-Type", "application/json")
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
@@ -1,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)
}
-58
View File
@@ -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,13 +0,0 @@
package be.simplenotes.app.filters
import me.liuwj.ktorm.database.Database
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
class TransactionFilter(private val db: Database) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = { request ->
db.useTransaction {
next(request)
}
}
}
-24
View File
@@ -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
)
}
}
@@ -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()) }
}
@@ -1,63 +0,0 @@
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.routes.Router
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.utils.StaticFileResolverImpl
import be.simplenotes.app.views.ErrorView
import be.simplenotes.shared.config.ServerConfig
import org.eclipse.jetty.server.ServerConnector
import org.http4k.core.Filter
import org.http4k.core.RequestContexts
import org.http4k.routing.RoutingHttpHandler
import org.http4k.server.ConnectorBuilder
import org.http4k.server.Jetty
import org.http4k.server.asServer
import org.koin.core.qualifier.named
import org.koin.core.qualifier.qualifier
import org.koin.dsl.module
import org.koin.dsl.onClose
import org.http4k.server.ServerConfig as Http4kServerConfig
val serverModule = module {
single(createdAtStart = true) { Server(get(), get()).start() } onClose { it?.stop() }
single { get<RoutingHttpHandler>().asServer(get()) }
single<Http4kServerConfig> {
val config = get<ServerConfig>()
val builder: ConnectorBuilder = { server: org.eclipse.jetty.server.Server ->
ServerConnector(server).apply {
port = config.port
host = config.host
}
}
Jetty(config.port, builder)
}
single<StaticFileResolver> { StaticFileResolverImpl(get()) }
single {
Router(
get(),
get(),
get(),
get(),
get(),
get(),
get(),
requiredAuth = get(AuthType.Required.qualifier),
optionalAuth = get(AuthType.Optional.qualifier),
apiAuth = get(named("apiAuthFilter")),
get(),
get(),
get(),
)()
}
single { RequestContexts() }
single<Filter>(AuthType.Optional.qualifier) { AuthFilter(get(), AuthType.Optional, get()) }
single<Filter>(AuthType.Required.qualifier) { AuthFilter(get(), AuthType.Required, get()) }
single { ErrorFilter(get()) }
single { TransactionFilter(get()) }
single { ErrorView(get()) }
}
-106
View File
@@ -1,106 +0,0 @@
package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.controllers.*
import be.simplenotes.app.filters.*
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.ServerFilters.InitialiseRequestContext
import org.http4k.routing.*
import org.http4k.routing.ResourceLoader.Companion.Classpath
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 transactionFilter: TransactionFilter,
private val contexts: RequestContexts,
) {
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(
basicRoutes,
optionalAuth.then(publicRoutes),
requiredAuth.then(protectedRoutes),
apiAuth.then(protectedApiRoutes),
apiRoutes,
)
val globalFilters = errorFilter
.then(InitialiseRequestContext(contexts))
.then(SecurityFilter)
.then(GZip())
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
-10
View File
@@ -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()))
@@ -1,12 +0,0 @@
host=localhost
port=8080
#
jdbcUrl=jdbc:h2:./notes-db;
driverClassName=org.h2.Driver
username=h2
password=
maximumPoolSize=10
connectionTimeout=3000
#
jwt.secret=PliLvfk7l4WF+cZJk66LR5Mpnh+ocbvJ2wfUCK2UCms=
jwt.validity=24
-1
View File
@@ -1 +0,0 @@
package be.simplenotes.app
+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",
"version": "1.0.0",
"scripts": {
"css": "NODE_ENV=dev MANIFEST=../app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../app/src/main/resources/static/styles.css",
"css-purge": "NODE_ENV=production MANIFEST=../app/src/main/resources/css-manifest.json postcss build src/styles.pcss --output ../app/src/main/resources/static/styles.css"
"//": "`gradle css`"
},
"dependencies": {
"autoprefixer": "^9.8.6",
+1 -1
View File
@@ -1,7 +1,7 @@
module.exports = {
purge: {
content: [
'../app/src/main/kotlin/views/**/*.kt'
process.env.PURGE
]
},
theme: {
-8
View File
@@ -1,8 +0,0 @@
#!/bin/sh
rm app/src/main/resources/css-manifest.json
rm app/src/main/resources/static/styles*
yarn --cwd css run css-purge \
&& docker build -t hubv/simplenotes:latest . \
&& docker push hubv/simplenotes:latest
+5 -5
View File
@@ -32,13 +32,13 @@ services:
- .env
environment:
- TZ=Europe/Brussels
- HOST=0.0.0.0
- JDBCURL=jdbc:mariadb://db:3306/simplenotes
- DRIVERCLASSNAME=org.mariadb.jdbc.Driver
- USERNAME=simplenotes
- SERVER_HOST=0.0.0.0
- DB_JDBC_URL=jdbc:mariadb://db:3306/simplenotes
- DB_DRIVER_CLASS_NAME=org.mariadb.jdbc.Driver
- DB_USERNAME=simplenotes
# .env:
# - JWT_SECRET
# - PASSWORD
# - DB_PASSWORD
ports:
- 127.0.0.1:8080:8080
healthcheck:
-97
View File
@@ -1,97 +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>parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>domain</artifactId>
<dependencies>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
</dependency>
<dependency>
<groupId>org.koin</groupId>
<artifactId>koin-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.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-serialization-json-jvm</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-compress</artifactId>
<version>1.20</version>
</dependency>
</dependencies>
</project>
-37
View File
@@ -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()) }
}
-4
View File
@@ -1,4 +0,0 @@
package be.simplenotes.domain.model
data class User(val username: String, val password: String)
data class PersistedUser(val username: String, val password: String, val id: Int)
@@ -1,20 +0,0 @@
package be.simplenotes.domain.security
import org.owasp.html.HtmlPolicyBuilder
object HtmlSanitizer {
private val htmlPolicy = HtmlPolicyBuilder()
.allowElements("a")
.allowCommonBlockElements()
.allowCommonInlineFormattingElements()
.allowElements("pre")
.allowAttributes("class").onElements("code")
.allowUrlProtocols("http", "https")
.allowAttributes("href").onElements("a")
.allowElements("input")
.allowAttributes("type", "checked", "disabled", "readonly").onElements("input")
.requireRelNofollowOnLinks()
.toFactory()!!
fun sanitize(unsafeHtml: String) = htmlPolicy.sanitize(unsafeHtml)!!
}
@@ -1,97 +0,0 @@
package be.simplenotes.domain.usecases
import arrow.core.Either
import arrow.core.extensions.fx
import be.simplenotes.domain.model.Note
import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.domain.security.HtmlSanitizer
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownParsingError
import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.domain.usecases.search.NoteSearcher
import be.simplenotes.domain.usecases.search.SearchTerms
import java.util.*
class NoteService(
private val markdownConverter: MarkdownConverter,
private val noteRepository: NoteRepository,
private val userRepository: UserRepository,
private val searcher: NoteSearcher,
) {
fun create(userId: Int, markdownText: String) = Either.fx<MarkdownParsingError, PersistedNote> {
val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.create(userId, it) }
searcher.indexNote(userId, persistedNote)
persistedNote
}
fun update(userId: Int, uuid: UUID, markdownText: String) = Either.fx<MarkdownParsingError, PersistedNote?> {
val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.update(userId, uuid, it) }
persistedNote?.let { searcher.updateIndex(userId, it) }
persistedNote
}
fun paginatedNotes(
userId: Int,
page: Int,
itemsPerPage: Int = 20,
tag: String? = null,
deleted: Boolean = false,
): PaginatedNotes {
val count = noteRepository.count(userId, tag, deleted)
val offset = (page - 1) * itemsPerPage
val numberOfPages = (count / itemsPerPage) + 1
val notes = if (count == 0) emptyList() else noteRepository.findAll(userId, itemsPerPage, offset, tag, deleted)
return PaginatedNotes(numberOfPages, notes)
}
fun find(userId: Int, uuid: UUID) = noteRepository.find(userId, uuid)
fun trash(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.delete(userId, uuid, permanent = false)
if (res) searcher.deleteIndex(userId, uuid)
return res
}
fun restore(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.restore(userId, uuid)
if (res) find(userId, uuid)?.let { note -> searcher.indexNote(userId, note) }
return res
}
fun delete(userId: Int, uuid: UUID): Boolean {
val res = noteRepository.delete(userId, uuid, permanent = true)
if (res) searcher.deleteIndex(userId, uuid)
return res
}
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
fun indexAll() {
val userIds = userRepository.findAll()
userIds.forEach { id ->
val notes = noteRepository.findAllDetails(id)
searcher.indexNotes(id, notes)
}
}
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
fun dropAllIndexes() = searcher.dropAll()
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
fun makePrivate(userId: Int, uuid: UUID) = noteRepository.makePrivate(userId, uuid)
fun findPublic(uuid: UUID) = noteRepository.findPublic(uuid)
}
data class PaginatedNotes(val pages: Int, val notes: List<PersistedNoteMetadata>)
@@ -1,27 +0,0 @@
package be.simplenotes.domain.usecases.users.delete
import arrow.core.Either
import arrow.core.extensions.fx
import arrow.core.rightIfNotNull
import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.domain.usecases.search.NoteSearcher
import be.simplenotes.domain.validation.UserValidations
internal class DeleteUseCaseImpl(
private val userRepository: UserRepository,
private val passwordHash: PasswordHash,
private val searcher: NoteSearcher,
) : DeleteUseCase {
override fun delete(form: DeleteForm) = Either.fx<DeleteError, Unit> {
val user = !UserValidations.validateDelete(form)
val persistedUser = !userRepository.find(user.username).rightIfNotNull { DeleteError.Unregistered }
!Either.cond(
passwordHash.verify(user.password, persistedUser.password),
{ Unit },
{ DeleteError.WrongPassword }
)
!Either.cond(userRepository.delete(persistedUser.id), { Unit }, { DeleteError.Unregistered })
searcher.dropIndex(persistedUser.id)
}
}
-5
View File
@@ -1,5 +0,0 @@
package be.simplenotes.domain
/**
* Empty file @see [root-package-declaration](https://discuss.kotlinlang.org/t/root-package-declaration-to-reduce-folder-clutter/2247/4)
*/
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
-77
View File
@@ -1,77 +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>parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>persistance</artifactId>
<dependencies>
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct</artifactId>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>domain</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>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.5</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,48 +0,0 @@
package be.simplenotes.persistance
import be.simplenotes.domain.usecases.repositories.NoteRepository
import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.persistance.converters.NoteConverter
import be.simplenotes.persistance.converters.NoteConverterImpl
import be.simplenotes.persistance.converters.UserConverter
import be.simplenotes.persistance.converters.UserConverterImpl
import be.simplenotes.persistance.notes.NoteRepositoryImpl
import be.simplenotes.persistance.users.UserRepositoryImpl
import be.simplenotes.shared.config.DataSourceConfig
import com.zaxxer.hikari.HikariConfig
import com.zaxxer.hikari.HikariDataSource
import me.liuwj.ktorm.database.Database
import org.koin.dsl.bind
import org.koin.dsl.module
import org.koin.dsl.onClose
import org.mapstruct.factory.Mappers
import javax.sql.DataSource
private fun hikariDataSource(conf: DataSourceConfig): HikariDataSource {
val hikariConfig = HikariConfig().also {
it.jdbcUrl = conf.jdbcUrl
it.driverClassName = conf.driverClassName
it.username = conf.username
it.password = conf.password
it.maximumPoolSize = conf.maximumPoolSize
it.connectionTimeout = conf.connectionTimeout
}
return HikariDataSource(hikariConfig)
}
val migrationModule = module {
single<DbMigrations> { DbMigrationsImpl(get(), get()) }
}
val persistanceModule = module {
single<NoteConverter> { NoteConverterImpl() }
single<UserConverter> { UserConverterImpl() }
single<UserRepository> { UserRepositoryImpl(get(), get()) }
single<NoteRepository> { NoteRepositoryImpl(get(), get()) }
single { hikariDataSource(get()) } bind DataSource::class onClose { it?.close() }
single {
get<DbMigrations>().migrate()
Database.connect(get<DataSource>())
}
single<DbHealthCheck> { DbHealthCheckImpl(get(), get()) }
}
@@ -1,16 +0,0 @@
package be.simplenotes.persistance.converters
import be.simplenotes.domain.model.PersistedUser
import be.simplenotes.domain.model.User
import be.simplenotes.persistance.users.UserEntity
import me.liuwj.ktorm.entity.Entity
import org.mapstruct.Mapper
@Mapper(uses = [UserEntityFactory::class])
internal interface UserConverter {
fun convertToUser(userEntity: UserEntity): User
fun convertToPersistedUser(userEntity: UserEntity): PersistedUser
fun convertToEntity(user: User): UserEntity
}
internal class UserEntityFactory : Entity.Factory<UserEntity>()
-1
View File
@@ -1 +0,0 @@
package be.simplenotes.persistance
@@ -1,121 +0,0 @@
package be.simplenotes.persistance.users
import be.simplenotes.domain.model.User
import be.simplenotes.domain.usecases.repositories.UserRepository
import be.simplenotes.persistance.DbMigrations
import be.simplenotes.persistance.migrationModule
import be.simplenotes.persistance.persistanceModule
import be.simplenotes.shared.config.DataSourceConfig
import me.liuwj.ktorm.database.*
import me.liuwj.ktorm.dsl.*
import me.liuwj.ktorm.entity.*
import org.assertj.core.api.Assertions.assertThat
import org.flywaydb.core.Flyway
import org.junit.jupiter.api.AfterAll
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.parallel.ResourceLock
import org.koin.dsl.koinApplication
import org.koin.dsl.module
import javax.sql.DataSource
@ResourceLock("h2")
internal class UserRepositoryImplTest {
// region setup
private val testModule = module {
single { dataSourceConfig() }
}
private val koinApp = koinApplication {
modules(persistanceModule, migrationModule, testModule)
}
private fun dataSourceConfig() = DataSourceConfig(
jdbcUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;",
driverClassName = "org.h2.Driver",
username = "h2",
password = "",
maximumPoolSize = 2,
connectionTimeout = 3000
)
private val koin = koinApp.koin
@AfterAll
fun afterAll() = koinApp.close()
private val migration = koin.get<DbMigrations>()
private val dataSource = koin.get<DataSource>()
private val userRepo = koin.get<UserRepository>()
private val db = koin.get<Database>()
@BeforeEach
fun beforeEach() {
Flyway.configure()
.dataSource(dataSource)
.load()
.clean()
migration.migrate()
}
// endregion setup
@Test
fun `insert user`() {
val user = User("username", "test")
assertThat(userRepo.create(user))
.isNotNull
.hasFieldOrPropertyWithValue("username", user.username)
.hasFieldOrPropertyWithValue("password", user.password)
assertThat(db.users.find { it.username eq user.username }).isNotNull
assertThat(db.users.toList()).hasSize(1)
assertThat(userRepo.create(user)).isNull()
}
@Nested
inner class Query {
@Test
fun `query existing user`() {
val user = User("username", "test")
userRepo.create(user)
val foundUserMaybe = userRepo.find(user.username)
assertThat(foundUserMaybe).isNotNull
val foundUser = foundUserMaybe!!
assertThat(foundUser).isEqualToIgnoringGivenFields(user, "id")
assertThat(userRepo.exists(user.username)).isTrue
assertThat(userRepo.find(foundUser.id)).isEqualTo(foundUser)
assertThat(userRepo.exists(foundUser.id)).isTrue
}
@Test
fun `query non existing user`() {
assertThat(userRepo.find("I don't exist")).isNull()
assertThat(userRepo.find(1)).isNull()
assertThat(userRepo.exists(1)).isFalse
assertThat(userRepo.exists("I don't exist")).isFalse
}
}
@Nested
inner class Delete {
@Test
fun `delete existing user`() {
val user = User("username", "test")
userRepo.create(user)
val foundUser = userRepo.find(user.username)!!
assertThat(userRepo.delete(foundUser.id)).isTrue
assertThat(db.users.toList()).isEmpty()
}
@Test
fun `delete non existing user`() {
assertThat(userRepo.delete(1)).isFalse
}
}
}
-247
View File
@@ -1,247 +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>parent</artifactId>
<version>1.0-SNAPSHOT</version>
<modules>
<module>persistance</module>
<module>app</module>
<module>domain</module>
<module>shared</module>
<module>search</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.10.5</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>
-66
View File
@@ -1,66 +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>parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>search</artifactId>
<properties>
<lucene.version>8.6.1</lucene.version>
</properties>
<dependencies>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>domain</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-core</artifactId>
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-analyzers-common</artifactId>
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.apache.lucene</groupId>
<artifactId>lucene-queryparser</artifactId>
<version>${lucene.version}</version>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>be.simplenotes</groupId>
<artifactId>shared</artifactId>
<version>1.0-SNAPSHOT</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
</dependencies>
</project>
-9
View File
@@ -1,9 +0,0 @@
package be.simplenotes.search
import be.simplenotes.domain.usecases.search.NoteSearcher
import org.koin.dsl.module
import java.nio.file.Path
val searchModule = module {
single<NoteSearcher> { NoteSearcherImpl(Path.of(".lucene")) }
}
-29
View File
@@ -1,29 +0,0 @@
package be.simplenotes.search.utils
import java.io.IOException
import java.nio.file.FileVisitResult
import java.nio.file.Files
import java.nio.file.Path
import java.nio.file.SimpleFileVisitor
import java.nio.file.attribute.BasicFileAttributes
internal fun rmdir(path: Path) {
try {
Files.walkFileTree(
path,
object : SimpleFileVisitor<Path>() {
override fun visitFile(file: Path, attrs: BasicFileAttributes?): FileVisitResult {
Files.delete(file)
return FileVisitResult.CONTINUE
}
override fun postVisitDirectory(dir: Path, exc: IOException?): FileVisitResult {
Files.delete(dir)
return FileVisitResult.CONTINUE
}
}
)
} catch (e: IOException) {
// This is fine
}
}
+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")
-41
View File
@@ -1,41 +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>parent</artifactId>
<groupId>be.simplenotes</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shared</artifactId>
<dependencies>
<dependency>
<groupId>io.arrow-kt</groupId>
<artifactId>arrow-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.natpryce</groupId>
<artifactId>hamkrest</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<executions>
<execution>
<goals>
<goal>test-jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
-1
View File
@@ -1 +0,0 @@
package be.simplenotes.shared
+33
View File
@@ -0,0 +1,33 @@
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-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.micronaut)
kapt(Libs.micronautProcessor)
testImplementation(Libs.junit)
testImplementation(Libs.assertJ)
testImplementation(Libs.http4kTestingHamkrest)
}
@@ -2,20 +2,26 @@ package be.simplenotes.app
import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory
import be.simplenotes.shared.config.ServerConfig as SimpleNotesServerConfig
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Singleton
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
@Singleton
class Server(
private val config: SimpleNotesServerConfig,
private val http4kServer: Http4kServer,
) {
private val logger = LoggerFactory.getLogger(javaClass)
@PostConstruct
fun start(): Server {
http4kServer.start()
logger.info("Listening on http://${config.host}:${config.port}")
logger.info("Listening on http://${config.host}:${http4kServer.port()}")
return this
}
@PreDestroy
fun stop() {
logger.info("Stopping server")
http4kServer.close()
@@ -0,0 +1,10 @@
package be.simplenotes.app
import io.micronaut.context.ApplicationContext
import java.lang.Runtime.getRuntime
fun main() {
val ctx = ApplicationContext.run()
ctx.createBean(Server::class.java)
getRuntime().addShutdownHook(Thread { ctx.stop() })
}
@@ -0,0 +1,79 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.OK
import org.http4k.lens.Path
import org.http4k.lens.uuid
import java.util.*
import javax.inject.Singleton
@Singleton
class ApiNoteController(
json: Json,
private val noteService: NoteService,
) {
fun createNote(request: Request, loggedInUser: LoggedInUser): Response {
val content = noteContentLens(request)
return noteService.create(loggedInUser, content).fold(
{ Response(BAD_REQUEST) },
{ uuidContentLens(UuidContent(it.uuid), Response(OK)) }
)
}
fun notes(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser): Response {
val notes = noteService.paginatedNotes(loggedInUser.userId, page = 1).notes
return persistedNotesMetadataLens(notes, Response(OK))
}
fun note(request: Request, loggedInUser: LoggedInUser): Response =
noteService.find(loggedInUser.userId, uuidLens(request))
?.let { persistedNoteLens(it, Response(OK)) }
?: Response(NOT_FOUND)
fun update(request: Request, loggedInUser: LoggedInUser): Response {
val content = noteContentLens(request)
return noteService.update(loggedInUser, uuidLens(request), content).fold(
{
Response(BAD_REQUEST)
},
{
if (it == null) Response(NOT_FOUND)
else Response(OK)
}
)
}
fun search(request: Request, loggedInUser: LoggedInUser): Response {
val query = searchContentLens(request)
val notes = noteService.search(loggedInUser.userId, query)
return persistedNotesMetadataLens(notes, Response(OK))
}
private val uuidContentLens = json.auto<UuidContent>().toLens()
private val noteContentLens = json.auto<NoteContent>().map { it.content }.toLens()
private val searchContentLens = json.auto<SearchContent>().map { it.query }.toLens()
private val persistedNotesMetadataLens = json.auto<List<PersistedNoteMetadata>>().toLens()
private val persistedNoteLens = json.auto<PersistedNote>().toLens()
private val uuidLens = Path.uuid().of("uuid")
}
@Serializable
data class NoteContent(val content: String)
@Serializable
data class UuidContent(@Contextual val uuid: UUID)
@Serializable
data class SearchContent(@Contextual val query: String)
@@ -0,0 +1,31 @@
package be.simplenotes.app.api
import be.simplenotes.app.extensions.auto
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.LoginForm
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.OK
import javax.inject.Singleton
@Singleton
class ApiUserController(
json: Json,
private val userService: UserService,
) {
private val tokenLens = json.auto<Token>().toLens()
private val loginFormLens = json.auto<LoginForm>().toLens()
fun login(request: Request) = userService
.login(loginFormLens(request))
.fold(
{ Response(BAD_REQUEST) },
{ tokenLens(Token(it), Response(OK)) }
)
}
@Serializable
data class Token(val token: String)
@@ -0,0 +1,15 @@
package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.BaseView
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import javax.inject.Singleton
@Singleton
class BaseController(private val view: BaseView) {
fun index(@Suppress("UNUSED_PARAMETER") request: Request, loggedInUser: LoggedInUser?) =
Response(OK).html(view.renderHome(loggedInUser))
}
@@ -0,0 +1,14 @@
package be.simplenotes.app.controllers
import be.simplenotes.domain.usecases.HealthCheckService
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import javax.inject.Singleton
@Singleton
class HealthCheckController(private val healthCheckService: HealthCheckService) {
fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
if (healthCheckService.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
}
@@ -2,13 +2,12 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect
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.markdown.InvalidMeta
import be.simplenotes.domain.usecases.markdown.MissingMeta
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.Request
import org.http4k.core.Response
@@ -18,25 +17,35 @@ import org.http4k.core.Status.Companion.OK
import org.http4k.core.body.form
import org.http4k.routing.path
import java.util.*
import javax.inject.Singleton
import kotlin.math.abs
@Singleton
class NoteController(
private val view: NoteView,
private val noteService: NoteService,
) {
fun new(request: Request, jwtPayload: JwtPayload): Response {
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(jwtPayload))
fun new(request: Request, loggedInUser: LoggedInUser): Response {
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(loggedInUser))
val markdownForm = request.form("markdown") ?: ""
return noteService.create(jwtPayload.userId, markdownForm).fold(
return noteService.create(loggedInUser, markdownForm).fold(
{
val html = when (it) {
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm)
MissingMeta -> view.noteEditor(
loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
)
InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
)
is ValidationError -> view.noteEditor(
jwtPayload,
loggedInUser,
validationErrors = it.validationErrors,
textarea = markdownForm
)
@@ -49,66 +58,73 @@ 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 tag = request.query("tag")
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag)
val deletedCount = noteService.countDeleted(jwtPayload.userId)
return Response(OK).html(view.notes(jwtPayload, notes, currentPage, pages, deletedCount, tag = tag))
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag)
val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.notes(loggedInUser, notes, currentPage, pages, deletedCount, tag = tag))
}
fun search(request: Request, jwtPayload: JwtPayload): Response {
fun search(request: Request, loggedInUser: LoggedInUser): Response {
val query = request.form("search") ?: ""
val terms = parseSearchTerms(query)
val notes = noteService.search(jwtPayload.userId, terms)
val deletedCount = noteService.countDeleted(jwtPayload.userId)
return Response(OK).html(view.search(jwtPayload, notes, query, deletedCount))
val notes = noteService.search(loggedInUser.userId, query)
val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.search(loggedInUser, notes, query, deletedCount))
}
fun note(request: Request, jwtPayload: JwtPayload): Response {
fun note(request: Request, loggedInUser: LoggedInUser): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
if (request.method == Method.POST) {
if (request.form("delete") != null) {
return if (noteService.trash(jwtPayload.userId, noteUuid))
return if (noteService.trash(loggedInUser.userId, noteUuid))
Response.redirect("/notes") // TODO: flash cookie to show success ?
else
Response(NOT_FOUND) // TODO: show an error
}
if (request.form("public") != null) {
if (!noteService.makePublic(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND)
if (!noteService.makePublic(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
} 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)
return Response(OK).html(view.renderedNote(jwtPayload, note, shared = false))
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = false))
}
fun public(request: Request, jwtPayload: JwtPayload?): Response {
fun public(request: Request, loggedInUser: LoggedInUser?): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
val note = noteService.findPublic(noteUuid) ?: return Response(NOT_FOUND)
return Response(OK).html(view.renderedNote(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 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) {
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") ?: ""
return noteService.update(jwtPayload.userId, note.uuid, markdownForm).fold(
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
{
val html = when (it) {
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm)
MissingMeta -> view.noteEditor(
loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
)
InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
)
is ValidationError -> view.noteEditor(
jwtPayload,
loggedInUser,
validationErrors = it.validationErrors,
textarea = markdownForm
)
@@ -121,21 +137,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 tag = request.query("tag")
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag, deleted = true)
return Response(OK).html(view.trash(jwtPayload, notes, currentPage, pages))
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag, deleted = true)
return Response(OK).html(view.trash(loggedInUser, notes, currentPage, pages))
}
fun deleted(request: Request, jwtPayload: JwtPayload): Response {
fun deleted(request: Request, loggedInUser: LoggedInUser): Response {
val uuid = request.uuidPath() ?: return Response(NOT_FOUND)
return if (request.form("delete") != null)
if (noteService.delete(jwtPayload.userId, uuid))
if (noteService.delete(loggedInUser.userId, uuid))
Response.redirect("/notes/trash")
else
Response(NOT_FOUND)
else if (noteService.restore(jwtPayload.userId, uuid))
else if (noteService.restore(loggedInUser.userId, uuid))
Response.redirect("/notes/$uuid")
else
Response(NOT_FOUND)
@@ -2,24 +2,26 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.redirect
import be.simplenotes.app.views.SettingView
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.delete.DeleteError
import be.simplenotes.domain.usecases.users.delete.DeleteForm
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.SettingView
import org.http4k.core.*
import org.http4k.core.body.form
import org.http4k.core.cookie.invalidateCookie
import javax.inject.Singleton
@Singleton
class SettingsController(
private val userService: UserService,
private val settingView: SettingView,
) {
fun settings(request: Request, jwtPayload: JwtPayload): Response {
fun settings(request: Request, loggedInUser: LoggedInUser): Response {
if (request.method == Method.GET)
return Response(Status.OK).html(settingView.settings(jwtPayload))
return Response(Status.OK).html(settingView.settings(loggedInUser))
val deleteForm = request.deleteForm(jwtPayload)
val deleteForm = request.deleteForm(loggedInUser)
val result = userService.delete(deleteForm)
return result.fold(
@@ -28,13 +30,13 @@ class SettingsController(
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer")
DeleteError.WrongPassword -> Response(Status.OK).html(
settingView.settings(
jwtPayload,
loggedInUser,
error = "Wrong password"
)
)
is DeleteError.InvalidForm -> Response(Status.OK).html(
settingView.settings(
jwtPayload,
loggedInUser,
validationErrors = it.validationErrors
)
)
@@ -53,23 +55,26 @@ class SettingsController(
.header("Content-Type", contentType)
}
fun export(request: Request, jwtPayload: JwtPayload): Response {
fun export(request: Request, loggedInUser: LoggedInUser): Response {
val isDownload = request.form("download") != null
return if (isDownload) {
val filename = "simplenotes-export-${jwtPayload.username}"
val filename = "simplenotes-export-${loggedInUser.username}"
if (request.form("format") == "zip") {
val zip = userService.exportAsZip(jwtPayload.userId)
val zip = userService.exportAsZip(loggedInUser.userId)
Response(Status.OK)
.with(attachment("$filename.zip", "application/zip"))
.body(zip)
} else
Response(Status.OK)
.with(attachment("$filename.json", "application/json"))
.body(userService.exportAsJson(jwtPayload.userId))
} else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json")
.body(userService.exportAsJson(loggedInUser.userId))
} else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header(
"Content-Type",
"application/json"
)
}
private fun Request.deleteForm(jwtPayload: JwtPayload) =
DeleteForm(jwtPayload.username, form("password"), form("checked") != null)
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
}
@@ -3,14 +3,14 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.isSecure
import be.simplenotes.app.extensions.redirect
import be.simplenotes.app.views.UserView
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.*
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
import be.simplenotes.domain.usecases.users.register.RegisterForm
import be.simplenotes.domain.usecases.users.register.UserExists
import be.simplenotes.shared.config.JwtConfig
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.UserView
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
@@ -21,15 +21,17 @@ import org.http4k.core.cookie.SameSite
import org.http4k.core.cookie.cookie
import org.http4k.core.cookie.invalidateCookie
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
@Singleton
class UserController(
private val userService: UserService,
private val userView: UserView,
private val jwtConfig: JwtConfig,
) {
fun register(request: Request, jwtPayload: JwtPayload?): Response {
fun register(request: Request, loggedInUser: LoggedInUser?): Response {
if (request.method == GET) return Response(OK).html(
userView.register(jwtPayload)
userView.register(loggedInUser)
)
val result = userService.register(request.registerForm())
@@ -38,12 +40,12 @@ class UserController(
{
val html = when (it) {
UserExists -> userView.register(
jwtPayload,
loggedInUser,
error = "User already exists"
)
is InvalidRegisterForm ->
userView.register(
jwtPayload,
loggedInUser,
validationErrors = it.validationErrors
)
}
@@ -58,9 +60,9 @@ class UserController(
private fun Request.registerForm() = RegisterForm(form("username"), form("password"))
private fun Request.loginForm(): LoginForm = registerForm()
fun login(request: Request, jwtPayload: JwtPayload?): Response {
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
if (request.method == GET) return Response(OK).html(
userView.login(jwtPayload)
userView.login(loggedInUser)
)
val result = userService.login(request.loginForm())
@@ -70,17 +72,17 @@ class UserController(
val html = when (it) {
Unregistered ->
userView.login(
jwtPayload,
loggedInUser,
error = "User does not exist"
)
WrongPassword ->
userView.login(
jwtPayload,
loggedInUser,
error = "Wrong password"
)
is InvalidLoginForm ->
userView.login(
jwtPayload,
loggedInUser,
validationErrors = it.validationErrors
)
}
@@ -0,0 +1,36 @@
package be.simplenotes.app.extensions
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.http4k.asString
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.FOUND
import org.http4k.core.Status.Companion.MOVED_PERMANENTLY
import org.http4k.lens.*
fun Response.html(html: String) = body(html)
.header("Content-Type", "text/html; charset=utf-8")
.header("Cache-Control", "no-cache")
fun Response.Companion.redirect(url: String, permanent: Boolean = false) =
Response(if (permanent) MOVED_PERMANENTLY else FOUND).header("Location", url)
fun Request.isSecure() = header("X-Forwarded-Proto")?.contains("https") ?: false
val bodyLens = httpBodyRoot(
listOf(Meta(true, "body", ParamMeta.ObjectParam, "body")),
ContentType.APPLICATION_JSON.withNoDirectives(),
ContentNegotiation.StrictNoDirective
).map(
{ it.payload.asString() },
{ Body(it) }
)
inline fun <reified T> Json.auto(): BiDiBodyLensSpec<T> = bodyLens.map(
{ decodeFromString(it) },
{ encodeToString(it) }
)
@@ -1,8 +1,8 @@
package be.simplenotes.app.filters
import be.simplenotes.app.extensions.html
import be.simplenotes.app.views.ErrorView
import be.simplenotes.app.views.ErrorView.Type.*
import be.simplenotes.views.ErrorView
import be.simplenotes.views.ErrorView.Type.*
import org.http4k.core.*
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
import org.http4k.core.Status.Companion.NOT_FOUND
@@ -10,7 +10,9 @@ import org.http4k.core.Status.Companion.NOT_IMPLEMENTED
import org.http4k.core.Status.Companion.SERVICE_UNAVAILABLE
import org.slf4j.LoggerFactory
import java.sql.SQLTransientException
import javax.inject.Singleton
@Singleton
class ErrorFilter(private val errorView: ErrorView) : Filter {
private val logger = LoggerFactory.getLogger(javaClass)
@@ -3,9 +3,13 @@ package be.simplenotes.app.filters
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Request
import org.http4k.core.Status.Companion.OK
object ImmutableFilter : Filter {
override fun invoke(next: HttpHandler) = { request: Request ->
next(request).header("Cache-Control", "public, max-age=31536000, immutable")
val res = next(request)
if (res.status == OK)
res.header("Cache-Control", "public, max-age=31536000, immutable")
else res
}
}
@@ -0,0 +1,24 @@
package be.simplenotes.app.filters.auth
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Request
import org.http4k.core.cookie.cookie
import org.http4k.lens.BiDiLens
typealias OptionalAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser?>
typealias RequiredAuthLens = BiDiLens<@JvmSuppressWildcards Request, @JvmSuppressWildcards LoggedInUser>
enum class JwtSource {
Header, Cookie
}
fun Request.bearerTokenCookie(): String? = cookie("Bearer")
?.value
?.trim()
fun Request.bearerTokenHeader(): String? =
header("Authorization")
?.trim()
?.takeIf { it.startsWith("Bearer") }
?.substringAfter("Bearer")
?.trim()
@@ -0,0 +1,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)
}
}
}
@@ -0,0 +1,43 @@
package be.simplenotes.app.jetty
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.ServerConnector
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.servlet.ServletContextHandler.SESSIONS
import org.eclipse.jetty.servlet.ServletHolder
import org.http4k.core.HttpHandler
import org.http4k.server.Http4kServer
import org.http4k.server.ServerConfig
import org.http4k.servlet.asServlet
class Jetty(private val port: Int, private val server: Server) : ServerConfig {
constructor(port: Int = 8000) : this(port, http(port))
constructor(port: Int, vararg inConnectors: ConnectorBuilder) : this(
port,
Server().apply {
inConnectors.forEach { addConnector(it(this)) }
}
)
override fun toServer(httpHandler: HttpHandler): Http4kServer {
server.insertHandler(httpHandler.toJettyHandler())
return object : Http4kServer {
override fun start(): Http4kServer = apply {
server.start()
}
override fun stop(): Http4kServer = apply { server.stop() }
override fun port(): Int = if (port > 0) port else server.uri.port
}
}
}
fun HttpHandler.toJettyHandler() = ServletContextHandler(SESSIONS).apply {
addServlet(ServletHolder(this@toJettyHandler.asServlet()), "/*")
}
typealias ConnectorBuilder = (Server) -> ServerConnector
fun http(httpPort: Int): ConnectorBuilder = { server: Server -> ServerConnector(server).apply { port = httpPort } }
@@ -0,0 +1,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()
}
@@ -2,21 +2,20 @@ package be.simplenotes.app.modules
import be.simplenotes.app.serialization.LocalDateTimeSerializer
import be.simplenotes.app.serialization.UuidSerializer
import io.micronaut.context.annotation.Factory
import kotlinx.serialization.json.Json
import kotlinx.serialization.modules.SerializersModule
import org.koin.dsl.module
import java.time.LocalDateTime
import java.util.*
import javax.inject.Singleton
val jsonModule = module {
single {
Json {
prettyPrint = true
serializersModule = get()
}
}
single {
SerializersModule {
@Factory
class JsonModule {
@Singleton
fun json() = Json {
prettyPrint = true
serializersModule = SerializersModule {
contextual(LocalDateTime::class, LocalDateTimeSerializer())
contextual(UUID::class, UuidSerializer())
}
@@ -0,0 +1,38 @@
package be.simplenotes.app.modules
import be.simplenotes.app.jetty.ConnectorBuilder
import be.simplenotes.app.jetty.Jetty
import be.simplenotes.app.routes.Router
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.config.ServerConfig
import io.micronaut.context.annotation.Factory
import org.eclipse.jetty.server.ServerConnector
import org.http4k.server.Http4kServer
import org.http4k.server.asServer
import javax.inject.Named
import javax.inject.Singleton
import org.eclipse.jetty.server.Server as JettyServer
import org.http4k.server.ServerConfig as Http4kServerConfig
@Factory
class ServerModule {
@Singleton
@Named("styles")
fun styles(resolver: StaticFileResolver) = resolver.resolve("styles.css")!!
@Singleton
fun http4kServer(router: Router, serverConfig: Http4kServerConfig): Http4kServer =
router().asServer(serverConfig)
@Singleton
fun http4kServerConfig(config: ServerConfig): Http4kServerConfig {
val builder: ConnectorBuilder = { server: JettyServer ->
ServerConnector(server).apply {
port = config.port
host = config.host
}
}
return Jetty(config.port, builder)
}
}
@@ -0,0 +1,47 @@
package be.simplenotes.app.routes
import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Method.*
import org.http4k.core.Request
import org.http4k.core.then
import org.http4k.routing.PathMethod
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class ApiRoutes(
private val apiUserController: ApiUserController,
private val apiNoteController: ApiNoteController,
@Named("api") private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
return routes(
"/login" bind POST to apiUserController::login,
with(apiNoteController) {
auth.then(
routes(
"/" bind GET to ::notes,
"/" bind POST to ::createNote,
"/search" bind POST to ::search,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind PUT to ::update,
)
).withBasePath("/notes")
}
).withBasePath("/api")
}
}
@@ -0,0 +1,60 @@
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.auth.OptionalAuthFilter
import be.simplenotes.app.filters.auth.OptionalAuthLens
import org.http4k.core.ContentType
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.then
import org.http4k.routing.*
import java.util.function.Supplier
import 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,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
infix fun PathMethod.to(action: PublicHandler) =
this to { req: Request -> action(req, authLens(req)) }
val staticHandler = ImmutableFilter.then(
static(
ResourceLoader.Classpath("/static"),
"woff2" to ContentType("font/woff2"),
"webmanifest" to ContentType("application/manifest+json")
)
)
return routes(
auth.then(
routes(
"/" bind GET to baseCtrl::index,
"/register" bind GET to userCtrl::register,
"/register" bind POST to userCtrl::register,
"/login" bind GET to userCtrl::login,
"/login" bind POST to userCtrl::login,
"/logout" bind POST to userCtrl::logout,
"/notes/public/{uuid}" bind GET to noteCtrl::public,
)
),
"/health" bind GET to healthCheckController::healthCheck,
staticHandler
)
}
}
@@ -0,0 +1,46 @@
package be.simplenotes.app.routes
import be.simplenotes.app.controllers.NoteController
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.then
import org.http4k.routing.PathMethod
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class NoteRoutes(
private val noteCtrl: NoteController,
private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
return auth.then(
with(noteCtrl) {
routes(
"/" bind GET to ::list,
"/" bind POST to ::search,
"/new" bind GET to ::new,
"/new" bind POST to ::new,
"/trash" bind GET to ::trash,
"/{uuid}" bind GET to ::note,
"/{uuid}" bind POST to ::note,
"/{uuid}/edit" bind GET to ::edit,
"/{uuid}/edit" bind POST to ::edit,
"/deleted/{uuid}" bind POST to ::deleted,
).withBasePath("/notes")
}
)
}
}
@@ -0,0 +1,8 @@
package be.simplenotes.app.routes
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Request
import org.http4k.core.Response
internal typealias PublicHandler = (Request, LoggedInUser?) -> Response
internal typealias ProtectedHandler = (Request, LoggedInUser) -> Response
@@ -0,0 +1,32 @@
package be.simplenotes.app.routes
import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.filters.SecurityFilter
import org.http4k.core.RequestContexts
import org.http4k.core.then
import org.http4k.filter.ResponseFilters.GZip
import org.http4k.filter.ServerFilters.InitialiseRequestContext
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Singleton
@Singleton
class Router(
private val errorFilter: ErrorFilter,
private val contexts: RequestContexts,
private val subRouters: List<Supplier<RoutingHttpHandler>>,
) {
operator fun invoke(): RoutingHttpHandler {
val routes = routes(
*subRouters.map { it.get() }.toTypedArray()
)
return errorFilter
.then(InitialiseRequestContext(contexts))
.then(SecurityFilter)
.then(GZip())
.then(routes)
}
}
@@ -0,0 +1,37 @@
package be.simplenotes.app.routes
import be.simplenotes.app.controllers.SettingsController
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import org.http4k.core.Method.GET
import org.http4k.core.Method.POST
import org.http4k.core.Request
import org.http4k.core.then
import org.http4k.routing.PathMethod
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
import java.util.function.Supplier
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class SettingsRoutes(
private val settingsController: SettingsController,
private val auth: RequiredAuthFilter,
@Named("required") private val authLens: RequiredAuthLens,
) : Supplier<RoutingHttpHandler> {
override fun get(): RoutingHttpHandler {
infix fun PathMethod.to(action: ProtectedHandler) =
this to { req: Request -> action(req, authLens(req)) }
return auth.then(
routes(
"/settings" bind GET to settingsController::settings,
"/settings" bind POST to settingsController::settings,
"/export" bind POST to settingsController::export,
)
)
}
}
@@ -10,7 +10,7 @@ import java.util.*
internal class UuidSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
get() = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
@@ -3,11 +3,13 @@ package be.simplenotes.app.utils
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import javax.inject.Singleton
interface StaticFileResolver {
fun resolve(name: String): String?
}
@Singleton
class StaticFileResolverImpl(json: Json) : StaticFileResolver {
private val mappings: Map<String, String>
@@ -13,4 +13,6 @@
<logger name="me.liuwj.ktorm.database" level="INFO"/>
<logger name="com.zaxxer.hikari" level="INFO"/>
<logger name="org.flywaydb.core" level="INFO"/>
<logger name="io.micronaut" level="INFO"/>
<logger name="io.micronaut.context.lifecycle" level="DEBUG"/>
</configuration>

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

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