1 Commits

Author SHA1 Message Date
hubert 32337ec308 Use mapstruct 2020-10-21 22:02:34 +02:00
208 changed files with 2247 additions and 2937 deletions
+9 -5
View File
@@ -1,6 +1,10 @@
# mariadb
MYSQL_ROOT_PASSWORD=
MYSQL_PASSWORD=
# simplenotes
DB_PASSWORD=
## can be generated with `openssl rand -base64 32`
JWT_SECRET=
#
## can be generated with `openssl rand -base64 32`
MYSQL_ROOT_PASSWORD=
#
## can be generated with `openssl rand -base64 32`
MYSQL_PASSWORD=
# password should be the same as mysql_password
PASSWORD=
+97 -5
View File
@@ -1,6 +1,24 @@
# Gradle
build/
.gradle
# 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
# IntelliJ
out/
@@ -10,8 +28,11 @@ out/
*.ipr
*.iws
# Vue
node_modules
/dist
# Local env files
.env
.env.local
.env.*.local
@@ -28,13 +49,85 @@ 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
@@ -43,4 +136,3 @@ jspm_packages/
# python
__pycache__
-5
View File
@@ -1,5 +0,0 @@
# 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
+24 -15
View File
@@ -1,3 +1,25 @@
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
@@ -20,21 +42,8 @@ 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", \
"--add-opens", \
"java.base/java.nio=ALL-UNNAMED", \
"-server", \
"-XX:+UnlockExperimentalVMOptions", \
"-Xms64m", \
"-Xmx256m", \
"-XX:+UseG1GC", \
"-XX:MaxGCPauseMillis=100", \
"-XX:+UseStringDeduplication", \
"-jar", \
"simplenotes.jar" \
]
CMD ["/myjdk/bin/java", "-server", "-XX:+UnlockExperimentalVMOptions", "-Xms64m", "-Xmx256m", "-XX:+UseG1GC", "-XX:MaxGCPauseMillis=100", "-XX:+UseStringDeduplication", "-jar", "app.jar"]
+2 -1
View File
@@ -15,4 +15,5 @@
## Configuration
The app is configured with environments variables.
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.
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.
+191
View File
@@ -0,0 +1,191 @@
<?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
@@ -0,0 +1,48 @@
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(),
)
}
@@ -2,26 +2,20 @@ package be.simplenotes.app
import org.http4k.server.Http4kServer
import org.slf4j.LoggerFactory
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Singleton
import be.simplenotes.config.ServerConfig as SimpleNotesServerConfig
import be.simplenotes.shared.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}:${http4kServer.port()}")
logger.info("Listening on http://${config.host}:${config.port}")
return this
}
@PreDestroy
fun stop() {
logger.info("Stopping server")
http4kServer.close()
+31
View File
@@ -0,0 +1,31 @@
package be.simplenotes.app
import be.simplenotes.app.extensions.addShutdownHook
import be.simplenotes.app.modules.*
import be.simplenotes.domain.domainModule
import be.simplenotes.persistance.migrationModule
import be.simplenotes.persistance.persistanceModule
import be.simplenotes.search.searchModule
import org.koin.core.context.startKoin
import org.koin.core.context.unloadKoinModules
fun main() {
startKoin {
modules(
serverModule,
persistanceModule,
migrationModule,
configModule,
baseModule,
userModule,
noteModule,
settingsModule,
domainModule,
searchModule,
apiModule,
jsonModule
)
}.addShutdownHook()
unloadKoinModules(listOf(migrationModule, configModule))
}
@@ -0,0 +1,77 @@
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)
@@ -0,0 +1,26 @@
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)
@@ -0,0 +1,13 @@
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))
}
@@ -5,10 +5,8 @@ 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 dbHealthCheck: DbHealthCheck) {
fun healthCheck(@Suppress("UNUSED_PARAMETER") request: Request) =
fun healthCheck(request: Request) =
if (dbHealthCheck.isOk()) Response(OK) else Response(SERVICE_UNAVAILABLE)
}
@@ -3,12 +3,12 @@ package be.simplenotes.app.controllers
import be.simplenotes.app.extensions.html
import be.simplenotes.app.extensions.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,35 +18,25 @@ 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, loggedInUser: LoggedInUser): Response {
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(loggedInUser))
fun new(request: Request, jwtPayload: JwtPayload): Response {
if (request.method == Method.GET) return Response(OK).html(view.noteEditor(jwtPayload))
val markdownForm = request.form("markdown") ?: ""
return noteService.create(loggedInUser, markdownForm).fold(
return noteService.create(jwtPayload.userId, markdownForm).fold(
{
val html = when (it) {
MissingMeta -> view.noteEditor(
loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
)
InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
)
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm)
is ValidationError -> view.noteEditor(
loggedInUser,
jwtPayload,
validationErrors = it.validationErrors,
textarea = markdownForm
)
@@ -59,74 +49,66 @@ class NoteController(
)
}
fun list(request: Request, loggedInUser: LoggedInUser): Response {
fun list(request: Request, jwtPayload: JwtPayload): Response {
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
val tag = request.query("tag")
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag)
val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.notes(loggedInUser, notes, currentPage, pages, deletedCount, tag = tag))
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))
}
fun search(request: Request, loggedInUser: LoggedInUser): Response {
fun search(request: Request, jwtPayload: JwtPayload): Response {
val query = request.form("search") ?: ""
val terms = parseSearchTerms(query)
val notes = noteService.search(loggedInUser.userId, terms)
val deletedCount = noteService.countDeleted(loggedInUser.userId)
return Response(OK).html(view.search(loggedInUser, notes, query, deletedCount))
val notes = noteService.search(jwtPayload.userId, terms)
val deletedCount = noteService.countDeleted(jwtPayload.userId)
return Response(OK).html(view.search(jwtPayload, notes, query, deletedCount))
}
fun note(request: Request, loggedInUser: LoggedInUser): Response {
fun note(request: Request, jwtPayload: JwtPayload): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
if (request.method == Method.POST) {
if (request.form("delete") != null) {
return if (noteService.trash(loggedInUser.userId, noteUuid))
return if (noteService.trash(jwtPayload.userId, noteUuid))
Response.redirect("/notes") // TODO: flash cookie to show success ?
else
Response(NOT_FOUND) // TODO: show an error
}
if (request.form("public") != null) {
if (!noteService.makePublic(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
if (!noteService.makePublic(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND)
} else if (request.form("private") != null) {
if (!noteService.makePrivate(loggedInUser.userId, noteUuid)) return Response(NOT_FOUND)
if (!noteService.makePrivate(jwtPayload.userId, noteUuid)) return Response(NOT_FOUND)
}
}
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = false))
val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND)
return Response(OK).html(view.renderedNote(jwtPayload, note, shared = false))
}
fun public(request: Request, loggedInUser: LoggedInUser?): Response {
fun public(request: Request, jwtPayload: JwtPayload?): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
val note = noteService.findPublic(noteUuid) ?: return Response(NOT_FOUND)
return Response(OK).html(view.renderedNote(loggedInUser, note, shared = true))
return Response(OK).html(view.renderedNote(jwtPayload, note, shared = true))
}
fun edit(request: Request, loggedInUser: LoggedInUser): Response {
fun edit(request: Request, jwtPayload: JwtPayload): Response {
val noteUuid = request.uuidPath() ?: return Response(NOT_FOUND)
val note = noteService.find(loggedInUser.userId, noteUuid) ?: return Response(NOT_FOUND)
val note = noteService.find(jwtPayload.userId, noteUuid) ?: return Response(NOT_FOUND)
if (request.method == Method.GET) {
return Response(OK).html(view.noteEditor(loggedInUser, textarea = note.markdown))
return Response(OK).html(view.noteEditor(jwtPayload, textarea = note.markdown))
}
val markdownForm = request.form("markdown") ?: ""
return noteService.update(loggedInUser, note.uuid, markdownForm).fold(
return noteService.update(jwtPayload.userId, note.uuid, markdownForm).fold(
{
val html = when (it) {
MissingMeta -> view.noteEditor(
loggedInUser,
error = "Missing note metadata",
textarea = markdownForm
)
InvalidMeta -> view.noteEditor(
loggedInUser,
error = "Invalid note metadata",
textarea = markdownForm
)
MissingMeta -> view.noteEditor(jwtPayload, error = "Missing note metadata", textarea = markdownForm)
InvalidMeta -> view.noteEditor(jwtPayload, error = "Invalid note metadata", textarea = markdownForm)
is ValidationError -> view.noteEditor(
loggedInUser,
jwtPayload,
validationErrors = it.validationErrors,
textarea = markdownForm
)
@@ -139,21 +121,21 @@ class NoteController(
)
}
fun trash(request: Request, loggedInUser: LoggedInUser): Response {
fun trash(request: Request, jwtPayload: JwtPayload): Response {
val currentPage = request.query("page")?.toIntOrNull()?.let(::abs) ?: 1
val tag = request.query("tag")
val (pages, notes) = noteService.paginatedNotes(loggedInUser.userId, currentPage, tag = tag, deleted = true)
return Response(OK).html(view.trash(loggedInUser, notes, currentPage, pages))
val (pages, notes) = noteService.paginatedNotes(jwtPayload.userId, currentPage, tag = tag, deleted = true)
return Response(OK).html(view.trash(jwtPayload, notes, currentPage, pages))
}
fun deleted(request: Request, loggedInUser: LoggedInUser): Response {
fun deleted(request: Request, jwtPayload: JwtPayload): Response {
val uuid = request.uuidPath() ?: return Response(NOT_FOUND)
return if (request.form("delete") != null)
if (noteService.delete(loggedInUser.userId, uuid))
if (noteService.delete(jwtPayload.userId, uuid))
Response.redirect("/notes/trash")
else
Response(NOT_FOUND)
else if (noteService.restore(loggedInUser.userId, uuid))
else if (noteService.restore(jwtPayload.userId, uuid))
Response.redirect("/notes/$uuid")
else
Response(NOT_FOUND)
@@ -2,26 +2,24 @@ 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, loggedInUser: LoggedInUser): Response {
fun settings(request: Request, jwtPayload: JwtPayload): Response {
if (request.method == Method.GET)
return Response(Status.OK).html(settingView.settings(loggedInUser))
return Response(Status.OK).html(settingView.settings(jwtPayload))
val deleteForm = request.deleteForm(loggedInUser)
val deleteForm = request.deleteForm(jwtPayload)
val result = userService.delete(deleteForm)
return result.fold(
@@ -30,13 +28,13 @@ class SettingsController(
DeleteError.Unregistered -> Response.redirect("/").invalidateCookie("Bearer")
DeleteError.WrongPassword -> Response(Status.OK).html(
settingView.settings(
loggedInUser,
jwtPayload,
error = "Wrong password"
)
)
is DeleteError.InvalidForm -> Response(Status.OK).html(
settingView.settings(
loggedInUser,
jwtPayload,
validationErrors = it.validationErrors
)
)
@@ -55,26 +53,23 @@ class SettingsController(
.header("Content-Type", contentType)
}
fun export(request: Request, loggedInUser: LoggedInUser): Response {
fun export(request: Request, jwtPayload: JwtPayload): Response {
val isDownload = request.form("download") != null
return if (isDownload) {
val filename = "simplenotes-export-${loggedInUser.username}"
val filename = "simplenotes-export-${jwtPayload.username}"
if (request.form("format") == "zip") {
val zip = userService.exportAsZip(loggedInUser.userId)
val zip = userService.exportAsZip(jwtPayload.userId)
Response(Status.OK)
.with(attachment("$filename.zip", "application/zip"))
.body(zip)
} else
Response(Status.OK)
.with(attachment("$filename.json", "application/json"))
.body(userService.exportAsJson(loggedInUser.userId))
} else Response(Status.OK).body(userService.exportAsJson(loggedInUser.userId)).header(
"Content-Type",
"application/json"
)
.body(userService.exportAsJson(jwtPayload.userId))
} else Response(Status.OK).body(userService.exportAsJson(jwtPayload.userId)).header("Content-Type", "application/json")
}
private fun Request.deleteForm(loggedInUser: LoggedInUser) =
DeleteForm(loggedInUser.username, form("password"), form("checked") != null)
private fun Request.deleteForm(jwtPayload: JwtPayload) =
DeleteForm(jwtPayload.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.config.JwtConfig
import be.simplenotes.app.views.UserView
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.users.login.*
import be.simplenotes.domain.usecases.users.register.InvalidRegisterForm
import be.simplenotes.domain.usecases.users.register.RegisterForm
import be.simplenotes.domain.usecases.users.register.UserExists
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.UserView
import be.simplenotes.shared.config.JwtConfig
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
@@ -21,17 +21,15 @@ 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, loggedInUser: LoggedInUser?): Response {
fun register(request: Request, jwtPayload: JwtPayload?): Response {
if (request.method == GET) return Response(OK).html(
userView.register(loggedInUser)
userView.register(jwtPayload)
)
val result = userService.register(request.registerForm())
@@ -40,12 +38,12 @@ class UserController(
{
val html = when (it) {
UserExists -> userView.register(
loggedInUser,
jwtPayload,
error = "User already exists"
)
is InvalidRegisterForm ->
userView.register(
loggedInUser,
jwtPayload,
validationErrors = it.validationErrors
)
}
@@ -60,9 +58,9 @@ class UserController(
private fun Request.registerForm() = RegisterForm(form("username"), form("password"))
private fun Request.loginForm(): LoginForm = registerForm()
fun login(request: Request, loggedInUser: LoggedInUser?): Response {
fun login(request: Request, jwtPayload: JwtPayload?): Response {
if (request.method == GET) return Response(OK).html(
userView.login(loggedInUser)
userView.login(jwtPayload)
)
val result = userService.login(request.loginForm())
@@ -72,17 +70,17 @@ class UserController(
val html = when (it) {
Unregistered ->
userView.login(
loggedInUser,
jwtPayload,
error = "User does not exist"
)
WrongPassword ->
userView.login(
loggedInUser,
jwtPayload,
error = "Wrong password"
)
is InvalidLoginForm ->
userView.login(
loggedInUser,
jwtPayload,
validationErrors = it.validationErrors
)
}
@@ -0,0 +1,17 @@
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
@@ -0,0 +1,12 @@
package be.simplenotes.app.extensions
import org.koin.core.KoinApplication
import kotlin.concurrent.thread
fun KoinApplication.addShutdownHook() {
Runtime.getRuntime().addShutdownHook(
thread(start = false) {
close()
}
)
}
@@ -0,0 +1,15 @@
package be.simplenotes.app.extensions
import kotlinx.html.*
class SUMMARY(consumer: TagConsumer<*>) :
HTMLTag(
"summary", consumer, emptyMap(),
inlineTag = true,
emptyTag = false
),
HtmlInlineTag
fun DETAILS.summary(block: SUMMARY.() -> Unit = {}) {
SUMMARY(consumer).visit(block)
}
+58
View File
@@ -0,0 +1,58 @@
package be.simplenotes.app.filters
import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.JwtPayloadExtractor
import org.http4k.core.*
import org.http4k.core.Status.Companion.UNAUTHORIZED
import org.http4k.core.cookie.cookie
enum class AuthType {
Optional, Required
}
private const val authKey = "auth"
class AuthFilter(
private val extractor: JwtPayloadExtractor,
private val authType: AuthType,
private val ctx: RequestContexts,
private val source: JwtSource = JwtSource.Cookie,
private val redirect: Boolean = true,
) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = {
val token = when (source) {
JwtSource.Header -> it.bearerTokenHeader()
JwtSource.Cookie -> it.bearerTokenCookie()
}
val jwtPayload = token?.let { token -> extractor(token) }
when {
jwtPayload != null -> {
ctx[it][authKey] = jwtPayload
next(it)
}
authType == AuthType.Required -> {
if (redirect) Response.redirect("/login")
else Response(UNAUTHORIZED)
}
else -> next(it)
}
}
}
fun Request.jwtPayload(ctx: RequestContexts): JwtPayload? = ctx[this][authKey]
enum class JwtSource {
Header, Cookie
}
private fun Request.bearerTokenCookie(): String? = cookie("Bearer")
?.value
?.trim()
private fun Request.bearerTokenHeader(): String? =
header("Authorization")
?.trim()
?.takeIf { it.startsWith("Bearer") }
?.substringAfter("Bearer")
?.trim()
@@ -1,8 +1,8 @@
package be.simplenotes.app.filters
import be.simplenotes.app.extensions.html
import be.simplenotes.views.ErrorView
import be.simplenotes.views.ErrorView.Type.*
import be.simplenotes.app.views.ErrorView
import be.simplenotes.app.views.ErrorView.Type.*
import org.http4k.core.*
import org.http4k.core.Status.Companion.INTERNAL_SERVER_ERROR
import org.http4k.core.Status.Companion.NOT_FOUND
@@ -10,9 +10,7 @@ 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,13 +3,9 @@ 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 ->
val res = next(request)
if (res.status == OK)
res.header("Cache-Control", "public, max-age=31536000, immutable")
else res
next(request).header("Cache-Control", "public, max-age=31536000, immutable")
}
}
@@ -3,9 +3,7 @@ package be.simplenotes.app.filters
import me.liuwj.ktorm.database.Database
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import javax.inject.Singleton
@Singleton
class TransactionFilter(private val db: Database) : Filter {
override fun invoke(next: HttpHandler): HttpHandler = { request ->
db.useTransaction {
+24
View File
@@ -0,0 +1,24 @@
package be.simplenotes.app.modules
import be.simplenotes.app.api.ApiNoteController
import be.simplenotes.app.api.ApiUserController
import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.JwtSource
import org.http4k.core.Filter
import org.koin.core.qualifier.named
import org.koin.dsl.module
val apiModule = module {
single { ApiUserController(get(), get()) }
single { ApiNoteController(get(), get()) }
single<Filter>(named("apiAuthFilter")) {
AuthFilter(
extractor = get(),
authType = AuthType.Required,
ctx = get(),
source = JwtSource.Header,
redirect = false
)
}
}
@@ -0,0 +1,11 @@
package be.simplenotes.app.modules
import be.simplenotes.app.Config
import org.koin.dsl.module
val configModule = module {
single { Config() }
single { get<Config>().dataSourceConfig }
single { get<Config>().jwtConfig }
single { get<Config>().serverConfig }
}
@@ -0,0 +1,29 @@
package be.simplenotes.app.modules
import be.simplenotes.app.controllers.*
import be.simplenotes.app.views.BaseView
import be.simplenotes.app.views.NoteView
import be.simplenotes.app.views.SettingView
import be.simplenotes.app.views.UserView
import org.koin.dsl.module
val userModule = module {
single { UserController(get(), get(), get()) }
single { UserView(get()) }
}
val baseModule = module {
single { HealthCheckController(get()) }
single { BaseController(get()) }
single { BaseView(get()) }
}
val noteModule = module {
single { NoteController(get(), get()) }
single { NoteView(get()) }
}
val settingsModule = module {
single { SettingsController(get(), get()) }
single { SettingView(get()) }
}
@@ -2,20 +2,21 @@ 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
@Factory
class JsonModule {
@Singleton
fun json() = Json {
val jsonModule = module {
single {
Json {
prettyPrint = true
serializersModule = SerializersModule {
serializersModule = get()
}
}
single {
SerializersModule {
contextual(LocalDateTime::class, LocalDateTimeSerializer())
contextual(UUID::class, UuidSerializer())
}
@@ -0,0 +1,63 @@
package be.simplenotes.app.modules
import be.simplenotes.app.Server
import be.simplenotes.app.filters.AuthFilter
import be.simplenotes.app.filters.AuthType
import be.simplenotes.app.filters.ErrorFilter
import be.simplenotes.app.filters.TransactionFilter
import be.simplenotes.app.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
@@ -0,0 +1,106 @@
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,7 +10,7 @@ import java.util.*
internal class UuidSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor
get() = PrimitiveSerialDescriptor("UUID", PrimitiveKind.STRING)
get() = PrimitiveSerialDescriptor("LocalDateTime", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: UUID) {
encoder.encodeString(value.toString())
+10
View File
@@ -0,0 +1,10 @@
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,6 +1,6 @@
package be.simplenotes.app.utils
import be.simplenotes.search.SearchTerms
import be.simplenotes.domain.usecases.search.SearchTerms
private fun innerRegex(name: String) =
"""$name:['"](.*?)['"]""".toRegex()
@@ -3,13 +3,11 @@ 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>
@@ -1,21 +1,19 @@
package be.simplenotes.views
package be.simplenotes.app.views
import be.simplenotes.types.LoggedInUser
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.*
import kotlinx.html.ThScope.col
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class BaseView(@Named("styles") styles: String) : View(styles) {
fun renderHome(loggedInUser: LoggedInUser?) = renderPage(
class BaseView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun renderHome(jwtPayload: JwtPayload?) = renderPage(
title = "Home",
description = "A fast and simple note taking website",
loggedInUser = loggedInUser
jwtPayload = jwtPayload
) {
section("text-center my-2 p-2") {
h1("text-5xl casual") {
span("text-teal-300") { +"SimpleNotes " }
span("text-teal-300") { +"Simplenotes " }
+"- access your notes anywhere"
}
}
@@ -1,14 +1,12 @@
package be.simplenotes.views
package be.simplenotes.app.views
import be.simplenotes.views.components.Alert
import be.simplenotes.views.components.alert
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.Alert
import be.simplenotes.app.views.components.alert
import kotlinx.html.a
import kotlinx.html.div
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class ErrorView(@Named("styles") styles: String) : View(styles) {
class ErrorView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
enum class Type(val title: String) {
SqlTransientError("Database unavailable"),
@@ -16,7 +14,7 @@ class ErrorView(@Named("styles") styles: String) : View(styles) {
Other("Error"),
}
fun error(errorType: Type) = renderPage(errorType.title, loggedInUser = null) {
fun error(errorType: Type) = renderPage(errorType.title, jwtPayload = null) {
div("container mx-auto p-4") {
when (errorType) {
Type.SqlTransientError -> alert(
@@ -1,23 +1,21 @@
package be.simplenotes.views
package be.simplenotes.app.views
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.views.components.*
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.*
import be.simplenotes.domain.model.PersistedNote
import be.simplenotes.domain.model.PersistedNoteMetadata
import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError
import kotlinx.html.*
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class NoteView(@Named("styles") styles: String) : View(styles) {
class NoteView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun noteEditor(
loggedInUser: LoggedInUser,
jwtPayload: JwtPayload,
error: String? = null,
textarea: String? = null,
validationErrors: List<ValidationError> = emptyList(),
) = renderPage(title = "New note", loggedInUser = loggedInUser) {
) = renderPage(title = "New note", jwtPayload = jwtPayload) {
div("container mx-auto p-4") {
error?.let { alert(Alert.Warning, error) }
validationErrors.forEach {
@@ -40,9 +38,7 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
|tags: []
|---
|
""".trimMargin(
"|"
)
""".trimMargin("|")
}
submitButton("Save")
}
@@ -50,13 +46,13 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
}
fun notes(
loggedInUser: LoggedInUser,
jwtPayload: JwtPayload,
notes: List<PersistedNoteMetadata>,
currentPage: Int,
numberOfPages: Int,
numberOfDeletedNotes: Int,
tag: String?,
) = renderPage(title = "Notes", loggedInUser = loggedInUser) {
) = renderPage(title = "Notes", jwtPayload = jwtPayload) {
div("container mx-auto p-4") {
noteListHeader(numberOfDeletedNotes)
if (notes.isNotEmpty())
@@ -72,11 +68,11 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
}
fun search(
loggedInUser: LoggedInUser,
jwtPayload: JwtPayload,
notes: List<PersistedNoteMetadata>,
query: String,
numberOfDeletedNotes: Int,
) = renderPage("Notes", loggedInUser = loggedInUser) {
) = renderPage("Notes", jwtPayload = jwtPayload) {
div("container mx-auto p-4") {
noteListHeader(numberOfDeletedNotes, query)
noteTable(notes)
@@ -84,11 +80,11 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
}
fun trash(
loggedInUser: LoggedInUser,
jwtPayload: JwtPayload,
notes: List<PersistedNoteMetadata>,
currentPage: Int,
numberOfPages: Int,
) = renderPage(title = "Notes", loggedInUser = loggedInUser) {
) = renderPage(title = "Notes", jwtPayload = jwtPayload) {
div("container mx-auto p-4") {
div("flex justify-between mb-4") {
h1("text-2xl underline") { +"Deleted notes" }
@@ -120,9 +116,9 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
}
}
fun renderedNote(loggedInUser: LoggedInUser?, note: PersistedNote, shared: Boolean) = renderPage(
fun renderedNote(jwtPayload: JwtPayload?, note: PersistedNote, shared: Boolean) = renderPage(
note.meta.title,
loggedInUser = loggedInUser,
jwtPayload = jwtPayload,
scripts = listOf("/highlight.10.1.2.js", "/init-highlight.0.0.1.js")
) {
div("container mx-auto p-4") {
@@ -147,6 +143,7 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
}
if (!shared) {
noteActionForm(note)
publicPrivateForm(note)
if (note.public) {
p("my-4") {
@@ -169,30 +166,12 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
}
private fun DIV.noteActionForm(note: PersistedNote) {
form(method = FormMethod.post, classes = "inline flex space-x-2 justify-end mb-4") {
span("flex space-x-2 justify-end mb-4") {
a(
href = "/notes/${note.uuid}/edit",
classes = "btn btn-green"
classes = "btn btn-teal"
) { +"Edit" }
span {
button(
type = ButtonType.submit,
name = if (note.public) "private" else "public",
classes = "font-semibold border-b-4 ${if (note.public) "border-teal-200" else "border-green-500"}" +
" p-2 rounded-l bg-teal-200 text-gray-800"
) {
+"Private"
}
button(
type = ButtonType.submit,
name = if (note.public) "private" else "public",
classes = "font-semibold border-b-4 " +
(if (!note.public) "border-teal-200" else "border-green-500") +
" p-2 rounded-r bg-teal-200 text-gray-800"
) {
+"Public"
}
}
form(method = FormMethod.post, classes = "inline") {
button(
type = ButtonType.submit,
name = "delete",
@@ -200,4 +179,23 @@ class NoteView(@Named("styles") styles: String) : View(styles) {
) { +"Delete" }
}
}
}
private fun DIV.publicPrivateForm(note: PersistedNote) {
span("flex space-x-2 justify-end mb-4") {
form(method = FormMethod.post, classes = "ml-auto ") {
button(
type = ButtonType.submit,
name = if (note.public) "private" else "public",
classes = "btn btn-teal"
) {
if (note.public)
+"This note is public, do you want to make it private ?"
else
+"This note is private, do you want to make it public ?"
}
}
}
}
}
@@ -1,52 +1,47 @@
package be.simplenotes.views
package be.simplenotes.app.views
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.components.Alert
import be.simplenotes.views.components.alert
import be.simplenotes.views.components.input
import be.simplenotes.views.extensions.summary
import be.simplenotes.app.extensions.summary
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.Alert
import be.simplenotes.app.views.components.alert
import be.simplenotes.app.views.components.input
import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError
import kotlinx.html.*
import kotlinx.html.ButtonType.submit
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class SettingView(@Named("styles") styles: String) : View(styles) {
class SettingView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun settings(
loggedInUser: LoggedInUser,
jwtPayload: JwtPayload,
error: String? = null,
validationErrors: List<ValidationError> = emptyList(),
) = renderPage("Settings", loggedInUser = loggedInUser) {
) = renderPage("Settings", jwtPayload = jwtPayload) {
div("container mx-auto") {
section("m-4 p-4 bg-gray-800 rounded") {
h1("text-xl") {
+"Welcome "
span("text-teal-200 font-semibold") { +loggedInUser.username }
span("text-teal-200 font-semibold") { +jwtPayload.username }
}
}
section("m-4 p-2 bg-gray-800 rounded flex flex-wrap justify-around items-end") {
section("m-4 p-4 bg-gray-800 rounded flex justify-around") {
form(classes = "m-2", method = FormMethod.post, action = "/export") {
button(
name = "display",
form(method = FormMethod.post, action = "/export") {
button(name = "display",
classes = "inline btn btn-teal block",
type = submit
) { +"Display my data" }
type = submit) { +"Display my data" }
}
form(classes = "m-2", method = FormMethod.post, action = "/export") {
form(method = FormMethod.post, action = "/export") {
div {
listOf("json", "zip").forEach { format ->
div {
radioInput(name = "format") {
id = format
attributes["value"] = format
if (format == "json") attributes["checked"] = ""
else attributes["class"] = "ml-4"
if(format == "json") attributes["checked"] = ""
}
label(classes = "ml-2") {
attributes["for"] = format
@@ -1,25 +1,23 @@
package be.simplenotes.views
package be.simplenotes.app.views
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.components.Alert
import be.simplenotes.views.components.alert
import be.simplenotes.views.components.input
import be.simplenotes.views.components.submitButton
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.Alert
import be.simplenotes.app.views.components.alert
import be.simplenotes.app.views.components.input
import be.simplenotes.app.views.components.submitButton
import be.simplenotes.domain.security.JwtPayload
import io.konform.validation.ValidationError
import kotlinx.html.*
import javax.inject.Named
import javax.inject.Singleton
@Singleton
class UserView(@Named("styles") styles: String) : View(styles) {
class UserView(staticFileResolver: StaticFileResolver) : View(staticFileResolver) {
fun register(
loggedInUser: LoggedInUser?,
jwtPayload: JwtPayload?,
error: String? = null,
validationErrors: List<ValidationError> = emptyList(),
) = accountForm(
"Register",
"Registration page",
loggedInUser,
jwtPayload,
error,
validationErrors,
"Create an account",
@@ -30,11 +28,11 @@ class UserView(@Named("styles") styles: String) : View(styles) {
}
fun login(
loggedInUser: LoggedInUser?,
jwtPayload: JwtPayload?,
error: String? = null,
validationErrors: List<ValidationError> = emptyList(),
new: Boolean = false,
) = accountForm("Login", "Login page", loggedInUser, error, validationErrors, "Sign In", "Sign In", new) {
) = accountForm("Login", "Login page", jwtPayload, error, validationErrors, "Sign In", "Sign In", new) {
+"Don't have an account yet? "
a(href = "/register", classes = "no-underline text-blue-500 hover:text-blue-400 font-bold") {
+"Create an account"
@@ -44,14 +42,14 @@ class UserView(@Named("styles") styles: String) : View(styles) {
private fun accountForm(
title: String,
description: String,
loggedInUser: LoggedInUser?,
jwtPayload: JwtPayload?,
error: String? = null,
validationErrors: List<ValidationError> = emptyList(),
h1: String,
submit: String,
new: Boolean = false,
footer: FlowContent.() -> Unit,
) = renderPage(title = title, description, loggedInUser = loggedInUser) {
) = renderPage(title = title, description, jwtPayload = jwtPayload) {
div("centered container mx-auto flex justify-center items-center") {
div("w-full md:w-1/2 lg:w-1/3 m-4") {
div("p-8 mb-6") {
@@ -1,16 +1,19 @@
package be.simplenotes.views
package be.simplenotes.app.views
import be.simplenotes.types.LoggedInUser
import be.simplenotes.views.components.navbar
import be.simplenotes.app.utils.StaticFileResolver
import be.simplenotes.app.views.components.navbar
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.*
import kotlinx.html.stream.appendHTML
abstract class View(private val styles: String) {
abstract class View(staticFileResolver: StaticFileResolver) {
private val styles = staticFileResolver.resolve("styles.css")!!
fun renderPage(
title: String,
description: String? = null,
loggedInUser: LoggedInUser?,
jwtPayload: JwtPayload?,
scripts: List<String> = emptyList(),
body: MAIN.() -> Unit = {},
) = buildString {
@@ -22,7 +25,7 @@ abstract class View(private val styles: String) {
meta(name = "viewport", content = "width=device-width, initial-scale=1")
title("$title - SimpleNotes")
description?.let { meta(name = "description", content = it) }
link(rel = "preload", href = "/recursive-0.0.1.woff2") {
link(rel = "preload", href = "/recursive-0.0.1.woff2"){
attributes["as"] = "font"
attributes["type"] = "font/woff2"
attributes["crossorigin"] = "anonymous"
@@ -34,7 +37,7 @@ abstract class View(private val styles: String) {
}
}
body("bg-gray-900 text-white") {
navbar(loggedInUser)
navbar(jwtPayload)
main { body() }
}
}
@@ -1,8 +1,8 @@
package be.simplenotes.views.components
package be.simplenotes.app.views.components
import kotlinx.html.*
internal fun FlowContent.alert(type: Alert, title: String, details: String? = null, multiline: Boolean = false) {
fun FlowContent.alert(type: Alert, title: String, details: String? = null, multiline: Boolean = false) {
val colors = when (type) {
Alert.Success -> "bg-green-500 border border-green-400 text-gray-800"
Alert.Warning -> "bg-red-500 border border-red-400 text-red-200"
@@ -17,6 +17,6 @@ internal fun FlowContent.alert(type: Alert, title: String, details: String? = nu
}
}
internal enum class Alert {
enum class Alert {
Success, Warning
}
@@ -1,13 +1,13 @@
package be.simplenotes.views.components
package be.simplenotes.app.views.components
import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.views.utils.toTimeAgo
import be.simplenotes.app.utils.toTimeAgo
import be.simplenotes.domain.model.PersistedNoteMetadata
import kotlinx.html.*
import kotlinx.html.ButtonType.submit
import kotlinx.html.FormMethod.post
import kotlinx.html.ThScope.col
internal fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") {
fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") {
table {
id = "notes"
thead {
@@ -25,8 +25,8 @@ internal fun FlowContent.deletedNoteTable(notes: List<PersistedNoteMetadata>) =
td("text-center") { +updatedAt.toTimeAgo() }
td { tags(tags) }
td("text-center") {
form(method = post, action = "/notes/deleted/$uuid") {
button(classes = "btn btn-red mb-2", type = submit, name = "delete") {
form(classes = "inline", method = post, action = "/notes/deleted/$uuid") {
button(classes = "btn btn-red", type = submit, name = "delete") {
+"Delete permanently"
}
button(classes = "ml-2 btn btn-green", type = submit, name = "restore") {
@@ -1,9 +1,9 @@
package be.simplenotes.views.components
package be.simplenotes.app.views.components
import kotlinx.html.*
import kotlinx.html.ButtonType.submit
internal fun FlowContent.input(
fun FlowContent.input(
type: InputType = InputType.text,
placeholder: String,
id: String,
@@ -26,7 +26,7 @@ internal fun FlowContent.input(
}
}
internal fun FlowContent.submitButton(text: String) {
fun FlowContent.submitButton(text: String) {
div("flex items-center mt-6") {
button(
type = submit,
@@ -1,9 +1,9 @@
package be.simplenotes.views.components
package be.simplenotes.app.views.components
import be.simplenotes.types.LoggedInUser
import be.simplenotes.domain.security.JwtPayload
import kotlinx.html.*
internal fun BODY.navbar(loggedInUser: LoggedInUser?) {
fun BODY.navbar(jwtPayload: JwtPayload?) {
nav {
id = "navbar"
a("/") {
@@ -12,7 +12,7 @@ internal fun BODY.navbar(loggedInUser: LoggedInUser?) {
}
ul("space-x-2") {
id = "navigation"
if (loggedInUser != null) {
if (jwtPayload != null) {
val links = listOf(
"/notes" to "Notes",
"/settings" to "Settings",
@@ -1,10 +1,10 @@
package be.simplenotes.views.components
package be.simplenotes.app.views.components
import kotlinx.html.*
import kotlinx.html.ButtonType.submit
import kotlinx.html.FormMethod.post
internal fun DIV.noteListHeader(numberOfDeletedNotes: Int, query: String = "") {
fun DIV.noteListHeader(numberOfDeletedNotes: Int, query: String = "") {
div("flex justify-between mb-4") {
h1("text-2xl underline") { +"Notes" }
span {
@@ -1,11 +1,11 @@
package be.simplenotes.views.components
package be.simplenotes.app.views.components
import be.simplenotes.types.PersistedNoteMetadata
import be.simplenotes.views.utils.toTimeAgo
import be.simplenotes.app.utils.toTimeAgo
import be.simplenotes.domain.model.PersistedNoteMetadata
import kotlinx.html.*
import kotlinx.html.ThScope.col
internal fun FlowContent.noteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") {
fun FlowContent.noteTable(notes: List<PersistedNoteMetadata>) = div("overflow-x-auto") {
table {
id = "notes"
thead {
@@ -0,0 +1,12 @@
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

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 814 B

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

+1
View File
@@ -0,0 +1 @@
package be.simplenotes.app
@@ -1,23 +1,15 @@
package be.simplenotes.app.filters
import be.simplenotes.app.filters.auth.OptionalAuthFilter
import be.simplenotes.app.filters.auth.OptionalAuthLens
import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens
import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.JwtPayload
import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.types.LoggedInUser
import be.simplenotes.shared.config.JwtConfig
import com.natpryce.hamkrest.assertion.assertThat
import io.micronaut.context.BeanContext
import io.micronaut.inject.qualifiers.Qualifiers
import org.http4k.core.*
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.RequestContexts
import org.http4k.core.Response
import org.http4k.core.Status.Companion.FOUND
import org.http4k.core.Status.Companion.OK
import org.http4k.core.cookie.cookie
import org.http4k.core.then
import org.http4k.filter.ServerFilters
import org.http4k.hamkrest.hasBody
import org.http4k.hamkrest.hasHeader
@@ -28,36 +20,22 @@ import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import java.util.concurrent.TimeUnit
internal class RequiredAuthFilterTest {
internal class AuthFilterTest {
// region setup
private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS)
private val simpleJwt = SimpleJwt(jwtConfig)
private val extractor = JwtPayloadExtractor(simpleJwt)
private val ctx = RequestContexts()
private val requiredAuth = AuthFilter(extractor, AuthType.Required, ctx)
private val optionalAuth = AuthFilter(extractor, AuthType.Optional, ctx)
private val beanCtx = BeanContext.build()
.registerSingleton(jwtConfig)
.start()
private inline fun <reified T> BeanContext.getBean(): T = getBean(T::class.java)
private inline fun <reified T> BeanContext.getBean(name: String): T =
getBean(T::class.java, Qualifiers.byName(name))
private val requiredAuth = beanCtx.getBean<RequiredAuthFilter>()
private val requiredLens = beanCtx.getBean<RequiredAuthLens>("required")
private val optionalAuth = beanCtx.getBean<OptionalAuthFilter>()
private val optionalLens = beanCtx.getBean<OptionalAuthLens>("optional")
private val ctx = beanCtx.getBean<RequestContexts>()
private val echoJwtPayloadHandler = { request: Request -> Response(OK).body(request.jwtPayload(ctx).toString()) }
private val app = ServerFilters.InitialiseRequestContext(ctx).then(
routes(
"/optional" bind GET to optionalAuth.then { request: Request ->
Response(OK).body(optionalLens(request).toString())
},
"/protected" bind GET to requiredAuth.then { request: Request ->
Response(OK).body(requiredLens(request).toString())
}
"/optional" bind GET to optionalAuth.then(echoJwtPayloadHandler),
"/protected" bind GET to requiredAuth.then(echoJwtPayloadHandler)
)
)
// endregion
@@ -80,7 +58,7 @@ internal class RequiredAuthFilterTest {
@Test
fun `it should allow a valid token`() {
val jwtPayload = LoggedInUser(1, "user")
val jwtPayload = JwtPayload(1, "user")
val token = simpleJwt.sign(jwtPayload)
val response = app(Request(GET, "/optional").cookie("Bearer", token))
assertThat(response, hasStatus(OK))
@@ -106,7 +84,7 @@ internal class RequiredAuthFilterTest {
@Test
fun `it should allow a valid token"`() {
val jwtPayload = LoggedInUser(1, "user")
val jwtPayload = JwtPayload(1, "user")
val token = simpleJwt.sign(jwtPayload)
val response = app(Request(GET, "/protected").cookie("Bearer", token))
assertThat(response, hasStatus(OK))
@@ -1,6 +1,6 @@
package be.simplenotes.app.utils
import be.simplenotes.search.SearchTerms
import be.simplenotes.domain.usecases.search.SearchTerms
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
@@ -30,9 +30,7 @@ internal class SearchTermsParserKtTest {
createResult("tag:'example' title:'other' end", title = "other", tag = "example", all = "end"),
createResult(
"tag:'example abc' title:'other with words' this is the end ",
title = "other with words",
tag = "example abc",
all = "this is the end"
title = "other with words", tag = "example abc", all = "this is the end"
),
)
-2
View File
@@ -1,2 +0,0 @@
org.gradle.caching=true
org.gradle.parallel=true
-20
View File
@@ -1,20 +0,0 @@
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")
}
@@ -1,77 +0,0 @@
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(":/")
}
}
}
@@ -1,45 +0,0 @@
@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"
}
@@ -1,15 +0,0 @@
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)
}
@@ -1,23 +0,0 @@
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)
}
}
}
@@ -1,38 +0,0 @@
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 !")
}
}
@@ -1,9 +0,0 @@
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")
}
@@ -1,26 +0,0 @@
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"
}
@@ -1,14 +0,0 @@
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")
}
@@ -1,25 +0,0 @@
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"
)
}
}
@@ -1,5 +0,0 @@
package be.simplenotes
plugins {
kotlin("plugin.serialization")
}
@@ -1,14 +0,0 @@
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
@@ -1,261 +0,0 @@
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
@@ -1,2 +0,0 @@
ch/qos/logback/core/db/**
ch/qos/logback/classic/db/**
@@ -1,5 +0,0 @@
org/checkerframework/**
org/intellij/**
com/google/errorprone/**
com/google/thirdparty/**
com/google/j2objc/**
+2 -1
View File
@@ -2,7 +2,8 @@
"name": "css",
"version": "1.0.0",
"scripts": {
"//": "`gradle css`"
"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"
},
"dependencies": {
"autoprefixer": "^9.8.6",
+1 -1
View File
@@ -1,7 +1,7 @@
module.exports = {
purge: {
content: [
process.env.PURGE
'../app/src/main/kotlin/views/**/*.kt'
]
},
theme: {
+8
View File
@@ -0,0 +1,8 @@
#!/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
- 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
- HOST=0.0.0.0
- JDBCURL=jdbc:mariadb://db:3306/simplenotes
- DRIVERCLASSNAME=org.mariadb.jdbc.Driver
- USERNAME=simplenotes
# .env:
# - JWT_SECRET
# - DB_PASSWORD
# - PASSWORD
ports:
- 127.0.0.1:8080:8080
healthcheck:
+97
View File
@@ -0,0 +1,97 @@
<?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
@@ -0,0 +1,37 @@
package be.simplenotes.domain
import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.domain.security.PasswordHash
import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.usecases.NoteService
import be.simplenotes.domain.usecases.UserService
import be.simplenotes.domain.usecases.export.ExportUseCase
import be.simplenotes.domain.usecases.export.ExportUseCaseImpl
import be.simplenotes.domain.usecases.markdown.MarkdownConverter
import be.simplenotes.domain.usecases.markdown.MarkdownConverterImpl
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
import be.simplenotes.domain.usecases.users.delete.DeleteUseCaseImpl
import be.simplenotes.domain.usecases.users.login.LoginUseCase
import be.simplenotes.domain.usecases.users.login.LoginUseCaseImpl
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
import be.simplenotes.domain.usecases.users.register.RegisterUseCaseImpl
import org.koin.dsl.module
val domainModule = module {
single<LoginUseCase> { LoginUseCaseImpl(get(), get(), get()) }
single<RegisterUseCase> { RegisterUseCaseImpl(get(), get()) }
single<DeleteUseCase> { DeleteUseCaseImpl(get(), get(), get()) }
single { UserService(get(), get(), get(), get()) }
single<PasswordHash> { BcryptPasswordHash() }
single { SimpleJwt(get()) }
single { JwtPayloadExtractor(get()) }
single {
NoteService(get(), get(), get(), get()).apply {
dropAllIndexes()
indexAll()
}
}
single<MarkdownConverter> { MarkdownConverterImpl() }
single<ExportUseCase> { ExportUseCaseImpl(get(), get()) }
}
@@ -1,4 +1,4 @@
package be.simplenotes.types
package be.simplenotes.domain.model
import kotlinx.serialization.Contextual
import kotlinx.serialization.Serializable
+4
View File
@@ -0,0 +1,4 @@
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)
@@ -0,0 +1,20 @@
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,16 +1,18 @@
package be.simplenotes.domain.security
import be.simplenotes.types.LoggedInUser
import be.simplenotes.domain.model.PersistedUser
import com.auth0.jwt.exceptions.JWTVerificationException
import javax.inject.Singleton
@Singleton
data class JwtPayload(val userId: Int, val username: String) {
constructor(user: PersistedUser) : this(user.id, user.username)
}
class JwtPayloadExtractor(private val jwt: SimpleJwt) {
operator fun invoke(token: String): LoggedInUser? = try {
operator fun invoke(token: String): JwtPayload? = try {
val decodedJWT = jwt.verifier.verify(token)
val id = decodedJWT.getClaim(userIdField).asInt() ?: null
val username = decodedJWT.getClaim(usernameField).asString() ?: null
id?.let { username?.let { LoggedInUser(id, username) } }
id?.let { username?.let { JwtPayload(id, username) } }
} catch (e: JWTVerificationException) {
null
} catch (e: IllegalArgumentException) {
@@ -1,19 +1,13 @@
package be.simplenotes.domain.security
import org.mindrot.jbcrypt.BCrypt
import javax.inject.Inject
import javax.inject.Singleton
internal interface PasswordHash {
fun crypt(password: String): String
fun verify(password: String, hashedPassword: String): Boolean
}
@Singleton
internal class BcryptPasswordHash constructor(test: Boolean) : PasswordHash {
@Inject
constructor() : this(false)
internal class BcryptPasswordHash(test: Boolean = false) : PasswordHash {
private val rounds = if (test) 4 else 10
override fun crypt(password: String) = BCrypt.hashpw(password, BCrypt.gensalt(rounds))!!
override fun verify(password: String, hashedPassword: String) = BCrypt.checkpw(password, hashedPassword)
@@ -1,26 +1,23 @@
package be.simplenotes.domain.security
import be.simplenotes.config.JwtConfig
import be.simplenotes.types.LoggedInUser
import be.simplenotes.shared.config.JwtConfig
import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm
import java.util.*
import java.util.concurrent.TimeUnit
import javax.inject.Singleton
internal const val userIdField = "i"
internal const val usernameField = "u"
@Singleton
class SimpleJwt(jwtConfig: JwtConfig) {
private val validityInMs = TimeUnit.MILLISECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit)
private val algorithm = Algorithm.HMAC256(jwtConfig.secret)
val verifier: JWTVerifier = JWT.require(algorithm).build()
fun sign(loggedInUser: LoggedInUser): String = JWT.create()
.withClaim(userIdField, loggedInUser.userId)
.withClaim(usernameField, loggedInUser.username)
fun sign(jwtPayload: JwtPayload): String = JWT.create()
.withClaim(userIdField, jwtPayload.userId)
.withClaim(usernameField, jwtPayload.username)
.withExpiresAt(getExpiration())
.sign(algorithm)
@@ -1,52 +1,43 @@
package be.simplenotes.domain.usecases
import arrow.core.computations.either
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.persistance.repositories.NoteRepository
import be.simplenotes.persistance.repositories.UserRepository
import be.simplenotes.search.NoteSearcher
import be.simplenotes.search.SearchTerms
import be.simplenotes.types.LoggedInUser
import be.simplenotes.types.Note
import be.simplenotes.types.PersistedNote
import be.simplenotes.types.PersistedNoteMetadata
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.*
import javax.annotation.PostConstruct
import javax.annotation.PreDestroy
import javax.inject.Singleton
@Singleton
class NoteService(
private val markdownConverter: MarkdownConverter,
private val noteRepository: NoteRepository,
private val userRepository: UserRepository,
private val searcher: NoteSearcher,
private val htmlSanitizer: HtmlSanitizer,
) {
fun create(user: LoggedInUser, markdownText: String) = either.eager<MarkdownParsingError, PersistedNote> {
fun create(userId: Int, markdownText: String) = Either.fx<MarkdownParsingError, PersistedNote> {
val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.create(user.userId, it) }
.map { noteRepository.create(userId, it) }
searcher.indexNote(user.userId, persistedNote)
searcher.indexNote(userId, persistedNote)
persistedNote
}
fun update(
user: LoggedInUser,
uuid: UUID,
markdownText: String,
) = either.eager<MarkdownParsingError, PersistedNote?> {
fun update(userId: Int, uuid: UUID, markdownText: String) = Either.fx<MarkdownParsingError, PersistedNote?> {
val persistedNote = !markdownConverter.renderDocument(markdownText)
.map { it.copy(html = htmlSanitizer.sanitize(user, it.html)) }
.map { it.copy(html = HtmlSanitizer.sanitize(it.html)) }
.map { Note(it.metadata, markdown = markdownText, html = it.html) }
.map { noteRepository.update(user.userId, uuid, it) }
.map { noteRepository.update(userId, uuid, it) }
persistedNote?.let { searcher.updateIndex(user.userId, it) }
persistedNote?.let { searcher.updateIndex(userId, it) }
persistedNote
}
@@ -86,9 +77,7 @@ class NoteService(
fun countDeleted(userId: Int) = noteRepository.count(userId, deleted = true)
@PostConstruct
fun indexAll() {
dropAllIndexes()
val userIds = userRepository.findAll()
userIds.forEach { id ->
val notes = noteRepository.findAllDetails(id)
@@ -98,7 +87,6 @@ class NoteService(
fun search(userId: Int, searchTerms: SearchTerms) = searcher.search(userId, searchTerms)
@PreDestroy
fun dropAllIndexes() = searcher.dropAll()
fun makePublic(userId: Int, uuid: UUID) = noteRepository.makePublic(userId, uuid)
@@ -4,9 +4,7 @@ import be.simplenotes.domain.usecases.export.ExportUseCase
import be.simplenotes.domain.usecases.users.delete.DeleteUseCase
import be.simplenotes.domain.usecases.users.login.LoginUseCase
import be.simplenotes.domain.usecases.users.register.RegisterUseCase
import javax.inject.Singleton
@Singleton
class UserService(
loginUseCase: LoginUseCase,
registerUseCase: RegisterUseCase,

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