Initial commit
This commit is contained in:
commit
c1c05276a2
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
target/
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.iws
|
||||||
|
*.db
|
||||||
230
pom.xml
Normal file
230
pom.xml
Normal file
@ -0,0 +1,230 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<project>
|
||||||
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
<groupId>be.simplenotes</groupId>
|
||||||
|
<artifactId>minimalnotes</artifactId>
|
||||||
|
<version>1.0-SNAPSHOT</version>
|
||||||
|
<properties>
|
||||||
|
<java.version>14</java.version>
|
||||||
|
<kotlin.version>1.4.10</kotlin.version>
|
||||||
|
<maven.compiler.target>${java.version}</maven.compiler.target>
|
||||||
|
<maven.compiler.source>${java.version}</maven.compiler.source>
|
||||||
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
|
<main.class>be.simplenotes/MinimalnotesKt</main.class>
|
||||||
|
</properties>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-stdlib-jdk8</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.koin</groupId>
|
||||||
|
<artifactId>koin-core</artifactId>
|
||||||
|
<version>2.1.6</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ch.qos.logback</groupId>
|
||||||
|
<artifactId>logback-classic</artifactId>
|
||||||
|
<version>1.2.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.github.ben-manes.caffeine</groupId>
|
||||||
|
<artifactId>caffeine</artifactId>
|
||||||
|
<version>2.8.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.http4k</groupId>
|
||||||
|
<artifactId>http4k-format-jackson</artifactId>
|
||||||
|
<version>3.261.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.http4k</groupId>
|
||||||
|
<artifactId>http4k-core</artifactId>
|
||||||
|
<version>3.261.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.http4k</groupId>
|
||||||
|
<artifactId>http4k-server-apache</artifactId>
|
||||||
|
<version>3.261.0</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.pebbletemplates</groupId>
|
||||||
|
<artifactId>pebble</artifactId>
|
||||||
|
<version>3.1.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<version>1.4.200</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jooq</groupId>
|
||||||
|
<artifactId>jooq</artifactId>
|
||||||
|
<version>3.13.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jooq</groupId>
|
||||||
|
<artifactId>jooq-meta</artifactId>
|
||||||
|
<version>3.13.4</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.jooq</groupId>
|
||||||
|
<artifactId>jooq-codegen</artifactId>
|
||||||
|
<version>3.13.4</version>
|
||||||
|
<scope>provided</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.zaxxer</groupId>
|
||||||
|
<artifactId>HikariCP</artifactId>
|
||||||
|
<version>3.4.5</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>de.mkammerer</groupId>
|
||||||
|
<artifactId>argon2-jvm</artifactId>
|
||||||
|
<version>2.7</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.auth0</groupId>
|
||||||
|
<artifactId>java-jwt</artifactId>
|
||||||
|
<version>3.10.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter</artifactId>
|
||||||
|
<version>5.7.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.junit.jupiter</groupId>
|
||||||
|
<artifactId>junit-jupiter-params</artifactId>
|
||||||
|
<version>5.7.0</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.assertj</groupId>
|
||||||
|
<artifactId>assertj-core</artifactId>
|
||||||
|
<version>3.17.2</version>
|
||||||
|
<scope>test</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.flywaydb</groupId>
|
||||||
|
<artifactId>flyway-core</artifactId>
|
||||||
|
<version>6.5.6</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
<repositories>
|
||||||
|
<repository>
|
||||||
|
<id>jcenter</id>
|
||||||
|
<url>https://jcenter.bintray.com</url>
|
||||||
|
</repository>
|
||||||
|
</repositories>
|
||||||
|
<build>
|
||||||
|
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||||
|
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-compiler-plugin</artifactId>
|
||||||
|
<version>3.8.1</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-dependency-plugin</artifactId>
|
||||||
|
<version>3.1.2</version>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-surefire-plugin</artifactId>
|
||||||
|
<version>3.0.0-M4</version>
|
||||||
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.maven.surefire</groupId>
|
||||||
|
<artifactId>surefire-junit-platform</artifactId>
|
||||||
|
<version>3.0.0-M4</version>
|
||||||
|
</dependency>
|
||||||
|
</dependencies>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
|
<artifactId>kotlin-maven-plugin</artifactId>
|
||||||
|
<version>${kotlin.version}</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<id>compile</id>
|
||||||
|
<phase>compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>compile</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
<execution>
|
||||||
|
<id>test-compile</id>
|
||||||
|
<phase>test-compile</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>test-compile</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<jvmTarget>${java.version}</jvmTarget>
|
||||||
|
<args>
|
||||||
|
<arg>-Xinline-classes</arg>
|
||||||
|
</args>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.apache.maven.plugins</groupId>
|
||||||
|
<artifactId>maven-shade-plugin</artifactId>
|
||||||
|
<version>3.2.4</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<phase>package</phase>
|
||||||
|
<goals>
|
||||||
|
<goal>shade</goal>
|
||||||
|
</goals>
|
||||||
|
<configuration>
|
||||||
|
<createDependencyReducedPom>false</createDependencyReducedPom>
|
||||||
|
<minimizeJar>false</minimizeJar>
|
||||||
|
<transformers>
|
||||||
|
<transformer
|
||||||
|
implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
|
||||||
|
<mainClass>${main.class}</mainClass>
|
||||||
|
</transformer>
|
||||||
|
</transformers>
|
||||||
|
</configuration>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
</plugin>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.jooq</groupId>
|
||||||
|
<artifactId>jooq-codegen-maven</artifactId>
|
||||||
|
<version>3.13.4</version>
|
||||||
|
<executions>
|
||||||
|
<execution>
|
||||||
|
<goals>
|
||||||
|
<goal>generate</goal>
|
||||||
|
</goals>
|
||||||
|
</execution>
|
||||||
|
</executions>
|
||||||
|
<configuration>
|
||||||
|
<jdbc>
|
||||||
|
<driver>org.h2.Driver</driver>
|
||||||
|
<url>jdbc:h2:./notes.db;MODE=MySQL;</url>
|
||||||
|
</jdbc>
|
||||||
|
<generator>
|
||||||
|
<database>
|
||||||
|
<name>org.jooq.meta.h2.H2Database</name>
|
||||||
|
<includes>.*</includes>
|
||||||
|
<excludes/>
|
||||||
|
<inputSchema/>
|
||||||
|
</database>
|
||||||
|
<target>
|
||||||
|
<packageName>be.simplenotes</packageName>
|
||||||
|
<directory>target/generated-sources/jooq</directory>
|
||||||
|
</target>
|
||||||
|
</generator>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
</project>
|
||||||
4
src/main/kotlin/Domain.kt
Normal file
4
src/main/kotlin/Domain.kt
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
package be.simplenotes
|
||||||
|
|
||||||
|
inline class UserId(val value: Long)
|
||||||
|
data class User(val id: UserId?, val username: String, val password: String)
|
||||||
77
src/main/kotlin/Minimalnotes.kt
Normal file
77
src/main/kotlin/Minimalnotes.kt
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package be.simplenotes
|
||||||
|
|
||||||
|
import be.simplenotes.extensions.addShutdownHook
|
||||||
|
import be.simplenotes.extensions.hikariDataSource
|
||||||
|
import be.simplenotes.extensions.registerSerializers
|
||||||
|
import be.simplenotes.repositories.UserRepository
|
||||||
|
import be.simplenotes.routes.RouteSupplier
|
||||||
|
import be.simplenotes.routes.UserRoutes
|
||||||
|
import be.simplenotes.routes.toRouter
|
||||||
|
import be.simplenotes.security.Argon2PasswordHash
|
||||||
|
import be.simplenotes.security.PasswordHash
|
||||||
|
import be.simplenotes.security.SimpleJwt
|
||||||
|
import be.simplenotes.serializers.UserIdSerializer
|
||||||
|
import be.simplenotes.serializers.UserSerializer
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||||
|
import org.flywaydb.core.Flyway
|
||||||
|
import org.http4k.core.then
|
||||||
|
import org.http4k.filter.ServerFilters
|
||||||
|
import org.http4k.format.Jackson
|
||||||
|
import org.http4k.routing.RoutingHttpHandler
|
||||||
|
import org.http4k.server.ApacheServer
|
||||||
|
import org.http4k.server.asServer
|
||||||
|
import org.jooq.SQLDialect
|
||||||
|
import org.jooq.impl.DSL
|
||||||
|
import org.koin.core.context.startKoin
|
||||||
|
import org.koin.dsl.bind
|
||||||
|
import org.koin.dsl.koinApplication
|
||||||
|
import org.koin.dsl.module
|
||||||
|
import org.koin.dsl.onClose
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
import javax.sql.DataSource
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
val jacksonModule = module {
|
||||||
|
single { UserIdSerializer() } bind StdSerializer::class
|
||||||
|
single { UserSerializer() } bind StdSerializer::class
|
||||||
|
single { Jackson.mapper }
|
||||||
|
single(createdAtStart = true) {
|
||||||
|
get<ObjectMapper>().registerSerializers(getAll())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val persistanceModule = module {
|
||||||
|
single { UserRepository(get(), get()) }
|
||||||
|
|
||||||
|
single {
|
||||||
|
hikariDataSource {
|
||||||
|
jdbcUrl = "jdbc:h2:./notes.db;MODE=MySQL;"
|
||||||
|
driverClassName = "org.h2.Driver"
|
||||||
|
maximumPoolSize = 2
|
||||||
|
}
|
||||||
|
} bind DataSource::class onClose { it?.close() }
|
||||||
|
|
||||||
|
single { Flyway.configure().dataSource(get()).load() }
|
||||||
|
single { get<Flyway>().migrate(); DSL.using(get<DataSource>(), SQLDialect.H2) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val securityModule = module {
|
||||||
|
single { SimpleJwt("super secret", 1, TimeUnit.HOURS) }
|
||||||
|
single<PasswordHash> { Argon2PasswordHash() }
|
||||||
|
}
|
||||||
|
|
||||||
|
val httpModule = module {
|
||||||
|
single { UserRoutes(get(), get()) } bind RouteSupplier::class
|
||||||
|
single { getAll<RouteSupplier>().toRouter() }
|
||||||
|
single(createdAtStart = true) {
|
||||||
|
ServerFilters.CatchLensFailure()
|
||||||
|
.then(get<RoutingHttpHandler>())
|
||||||
|
.asServer(ApacheServer(7000)).start()
|
||||||
|
} onClose { it?.close() }
|
||||||
|
}
|
||||||
|
|
||||||
|
startKoin {
|
||||||
|
modules(httpModule, securityModule, jacksonModule, persistanceModule)
|
||||||
|
}.koin.addShutdownHook()
|
||||||
|
}
|
||||||
7
src/main/kotlin/extensions/HikariExtension.kt
Normal file
7
src/main/kotlin/extensions/HikariExtension.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package be.simplenotes.extensions
|
||||||
|
|
||||||
|
import com.zaxxer.hikari.HikariConfig
|
||||||
|
import com.zaxxer.hikari.HikariDataSource
|
||||||
|
|
||||||
|
fun hikariDataSource(builder: HikariConfig.() -> Unit): HikariDataSource =
|
||||||
|
HikariDataSource(HikariConfig().apply(builder))
|
||||||
13
src/main/kotlin/extensions/JacksonExtension.kt
Normal file
13
src/main/kotlin/extensions/JacksonExtension.kt
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package be.simplenotes.extensions
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||||
|
|
||||||
|
fun ObjectMapper.registerSerializers(serializers: List<StdSerializer<*>>) {
|
||||||
|
registerModule(object : SimpleModule() {
|
||||||
|
init {
|
||||||
|
serializers.forEach { addSerializer(it) }
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
10
src/main/kotlin/extensions/KoinExtensions.kt
Normal file
10
src/main/kotlin/extensions/KoinExtensions.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package be.simplenotes.extensions
|
||||||
|
|
||||||
|
import org.koin.core.Koin
|
||||||
|
import kotlin.concurrent.thread
|
||||||
|
|
||||||
|
fun Koin.addShutdownHook() {
|
||||||
|
Runtime.getRuntime().addShutdownHook(thread(start = false) {
|
||||||
|
close()
|
||||||
|
})
|
||||||
|
}
|
||||||
7
src/main/kotlin/extensions/http4kExtensions.kt
Normal file
7
src/main/kotlin/extensions/http4kExtensions.kt
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package be.simplenotes.extensions
|
||||||
|
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.Status
|
||||||
|
|
||||||
|
fun Response.Companion.ok() = Response(Status.OK)
|
||||||
|
fun Response.Companion.notFound() = Response(Status.NOT_FOUND)
|
||||||
44
src/main/kotlin/repositories/UserRepository.kt
Normal file
44
src/main/kotlin/repositories/UserRepository.kt
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
package be.simplenotes.repositories
|
||||||
|
|
||||||
|
import be.simplenotes.User
|
||||||
|
import be.simplenotes.UserId
|
||||||
|
import be.simplenotes.public_.tables.Users.USERS
|
||||||
|
import be.simplenotes.security.PasswordHash
|
||||||
|
import com.github.benmanes.caffeine.cache.Caffeine
|
||||||
|
import org.jooq.DSLContext
|
||||||
|
import org.jooq.Record
|
||||||
|
import org.jooq.exception.DataAccessException
|
||||||
|
|
||||||
|
class UserRepository(private val db: DSLContext, private val passwordHash: PasswordHash) {
|
||||||
|
private val cache = Caffeine.newBuilder()
|
||||||
|
.maximumSize(10)
|
||||||
|
.build<UserId, User>()
|
||||||
|
|
||||||
|
@Throws(DataAccessException::class)
|
||||||
|
fun create(user: User) {
|
||||||
|
db.insertInto(USERS, USERS.USERNAME, USERS.PASSWORD)
|
||||||
|
.values(user.username, passwordHash.hash(user.password))
|
||||||
|
.execute()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun find(id: UserId): User? = cache.get(id) {
|
||||||
|
db.fetchOne(USERS, USERS.ID.eq(id.value))?.map(::userMapper)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun find(username: String, password: String): User? {
|
||||||
|
val user = db.fetchOne(USERS, USERS.USERNAME.eq(username))?.map(::userMapper) ?: return null
|
||||||
|
|
||||||
|
return if (passwordHash.verify(user.password, password))
|
||||||
|
user
|
||||||
|
else null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun userMapper(record: Record): User =
|
||||||
|
User(UserId(record[USERS.ID]), record[USERS.USERNAME], record[USERS.PASSWORD])
|
||||||
|
|
||||||
|
fun delete(id: UserId): Boolean {
|
||||||
|
val deleted = db.deleteFrom(USERS).where(USERS.ID.eq(id.value)).execute() == 1
|
||||||
|
if (deleted) cache.invalidate(id)
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
10
src/main/kotlin/routes/RouteSupplier.kt
Normal file
10
src/main/kotlin/routes/RouteSupplier.kt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package be.simplenotes.routes
|
||||||
|
|
||||||
|
import org.http4k.routing.RoutingHttpHandler
|
||||||
|
import org.http4k.routing.routes
|
||||||
|
|
||||||
|
interface RouteSupplier {
|
||||||
|
fun get(): RoutingHttpHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
fun List<RouteSupplier>.toRouter() = routes(*map { it.get() }.toTypedArray())
|
||||||
74
src/main/kotlin/routes/UserRoutes.kt
Normal file
74
src/main/kotlin/routes/UserRoutes.kt
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
package be.simplenotes.routes
|
||||||
|
|
||||||
|
import be.simplenotes.security.SimpleJwt
|
||||||
|
import be.simplenotes.User
|
||||||
|
import be.simplenotes.UserId
|
||||||
|
import be.simplenotes.extensions.notFound
|
||||||
|
import be.simplenotes.extensions.ok
|
||||||
|
import be.simplenotes.repositories.UserRepository
|
||||||
|
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||||
|
import org.http4k.core.Body
|
||||||
|
import org.http4k.core.Method.*
|
||||||
|
import org.http4k.core.Response
|
||||||
|
import org.http4k.core.cookie.Cookie
|
||||||
|
import org.http4k.core.cookie.cookie
|
||||||
|
import org.http4k.core.with
|
||||||
|
import org.http4k.format.Jackson.auto
|
||||||
|
import org.http4k.lens.ContentNegotiation
|
||||||
|
import org.http4k.lens.Cookies
|
||||||
|
import org.http4k.lens.Path
|
||||||
|
import org.http4k.lens.long
|
||||||
|
import org.http4k.routing.bind
|
||||||
|
import org.http4k.routing.routes
|
||||||
|
|
||||||
|
data class Message(val msg: String)
|
||||||
|
data class Login(val username: String, val password: String)
|
||||||
|
|
||||||
|
class UserRoutes(private val userRepository: UserRepository, private val simpleJwt: SimpleJwt) : RouteSupplier {
|
||||||
|
private val userLens = Body.auto<User>("user", ContentNegotiation.StrictNoDirective).toLens()
|
||||||
|
private val msgLens = Body.auto<Message>().toLens()
|
||||||
|
private val loginLens = Body.auto<Login>().toLens()
|
||||||
|
private val idLens = Path.long().map { UserId(it) }.of("id")
|
||||||
|
private val jwtLens = Cookies.optional("Bearer")
|
||||||
|
|
||||||
|
private fun userNotFound() = Response.notFound().with(msgLens of Message("not found"))
|
||||||
|
|
||||||
|
override fun get() = routes(
|
||||||
|
"/user" bind POST to {
|
||||||
|
userRepository.create(userLens(it))
|
||||||
|
Response.ok().with(msgLens of Message("created"))
|
||||||
|
},
|
||||||
|
"/user/{id}" bind GET to {
|
||||||
|
val user = userRepository.find(idLens(it))
|
||||||
|
user?.let {
|
||||||
|
Response.ok().with(userLens of user)
|
||||||
|
} ?: userNotFound()
|
||||||
|
},
|
||||||
|
"/user/{id}" bind DELETE to {
|
||||||
|
if (userRepository.delete(idLens(it)))
|
||||||
|
Response.ok().with(msgLens of Message("deleted"))
|
||||||
|
else userNotFound()
|
||||||
|
},
|
||||||
|
"/login" bind POST to {
|
||||||
|
val (username, password) = loginLens(it)
|
||||||
|
userRepository.find(username, password)?.let { user ->
|
||||||
|
val cookie = Cookie(
|
||||||
|
"Bearer", simpleJwt.sign(user.id!!)
|
||||||
|
)
|
||||||
|
Response.ok().with(userLens of user.copy(id = null)).cookie(cookie)
|
||||||
|
} ?: Response.notFound()
|
||||||
|
},
|
||||||
|
"/whoami" bind GET to {
|
||||||
|
jwtLens(it)?.value
|
||||||
|
?.let { token ->
|
||||||
|
try {
|
||||||
|
simpleJwt.extract(token)
|
||||||
|
} catch (e: JWTVerificationException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?.let { id -> Response.ok().with(userLens of userRepository.find(id)!!) }
|
||||||
|
?: Response.notFound()
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
14
src/main/kotlin/security/PasswordHash.kt
Normal file
14
src/main/kotlin/security/PasswordHash.kt
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
package be.simplenotes.security
|
||||||
|
|
||||||
|
import de.mkammerer.argon2.Argon2Factory
|
||||||
|
|
||||||
|
interface PasswordHash {
|
||||||
|
fun hash(password: String): String
|
||||||
|
fun verify(hash: String, password: String): Boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
class Argon2PasswordHash : PasswordHash {
|
||||||
|
private val argon2 = Argon2Factory.create()
|
||||||
|
override fun hash(password: String): String = argon2.hash(10, 65536 / 2, 1, password.encodeToByteArray())
|
||||||
|
override fun verify(hash: String, password: String) = argon2.verify(hash, password.encodeToByteArray())
|
||||||
|
}
|
||||||
33
src/main/kotlin/security/SimpleJwt.kt
Normal file
33
src/main/kotlin/security/SimpleJwt.kt
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
package be.simplenotes.security
|
||||||
|
|
||||||
|
import be.simplenotes.UserId
|
||||||
|
import com.auth0.jwt.JWT
|
||||||
|
import com.auth0.jwt.JWTVerifier
|
||||||
|
import com.auth0.jwt.algorithms.Algorithm
|
||||||
|
import com.auth0.jwt.exceptions.JWTVerificationException
|
||||||
|
import java.util.*
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
class SimpleJwt(secret: String, validity: Long, timeUnit: TimeUnit) {
|
||||||
|
private val validityInMs = TimeUnit.MILLISECONDS.convert(validity, timeUnit)
|
||||||
|
private val algorithm = Algorithm.HMAC256(secret)
|
||||||
|
|
||||||
|
private val idClaim = "id"
|
||||||
|
|
||||||
|
private val verifier: JWTVerifier = JWT.require(algorithm).build()
|
||||||
|
fun sign(id: UserId): String = JWT.create()
|
||||||
|
.withClaim(idClaim, id.value)
|
||||||
|
.withExpiresAt(getExpiration())
|
||||||
|
.sign(algorithm)
|
||||||
|
|
||||||
|
fun extract(token: String): UserId? = try {
|
||||||
|
val decodedJWT = verifier.verify(token)
|
||||||
|
decodedJWT.getClaim(idClaim).asLong()?.let { UserId(it) }
|
||||||
|
} catch (e: JWTVerificationException) {
|
||||||
|
null
|
||||||
|
} catch (e: IllegalArgumentException) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
|
||||||
|
}
|
||||||
24
src/main/kotlin/serializers/UserSerializers.kt
Normal file
24
src/main/kotlin/serializers/UserSerializers.kt
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package be.simplenotes.serializers
|
||||||
|
|
||||||
|
import be.simplenotes.User
|
||||||
|
import be.simplenotes.UserId
|
||||||
|
import com.fasterxml.jackson.core.JsonGenerator
|
||||||
|
import com.fasterxml.jackson.databind.SerializerProvider
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.StdSerializer
|
||||||
|
|
||||||
|
// We need this because inline classes..
|
||||||
|
class UserIdSerializer : StdSerializer<UserId>(UserId::class.java) {
|
||||||
|
override fun serialize(value: UserId, gen: JsonGenerator, provider: SerializerProvider) {
|
||||||
|
gen.writeObject(value.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class UserSerializer : StdSerializer<User>(User::class.java) {
|
||||||
|
override fun serialize(user: User, gen: JsonGenerator, provider: SerializerProvider) {
|
||||||
|
gen.writeStartObject()
|
||||||
|
gen.writeObjectField("id", user.id)
|
||||||
|
gen.writeStringField("username", user.username)
|
||||||
|
gen.writeStringField("password", user.password)
|
||||||
|
gen.writeEndObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
8
src/main/resources/db/migration/V1__Create_users.sql
Normal file
8
src/main/resources/db/migration/V1__Create_users.sql
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
create table Users
|
||||||
|
(
|
||||||
|
id bigint auto_increment primary key,
|
||||||
|
username varchar(50) not null,
|
||||||
|
password varchar(255) not null,
|
||||||
|
|
||||||
|
constraint username unique (username)
|
||||||
|
);
|
||||||
18
src/main/resources/logback.xml
Normal file
18
src/main/resources/logback.xml
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<configuration>
|
||||||
|
<appender class="ch.qos.logback.core.ConsoleAppender" name="STDOUT">
|
||||||
|
<withJansi>true</withJansi>
|
||||||
|
<encoder>
|
||||||
|
<pattern>%cyan(%d{YYYY-MM-dd HH:mm:ss.SSS}) [%thread] %highlight(%-5level) %green(%logger{36}) - %msg%n
|
||||||
|
</pattern>
|
||||||
|
</encoder>
|
||||||
|
</appender>
|
||||||
|
<root level="DEBUG">
|
||||||
|
<appender-ref ref="STDOUT"/>
|
||||||
|
</root>
|
||||||
|
<logger level="INFO" name="com.mitchellbosecke.pebble"/>
|
||||||
|
<logger level="WARN" name="com.zaxxer.hikari"/>
|
||||||
|
<logger level="INFO" name="org.flywaydb"/>
|
||||||
|
<logger level="WARN" name="org.flywaydb.core.internal.license.VersionPrinter"/>
|
||||||
|
<logger level="WARN" name="org.jooq.Constants"/>
|
||||||
|
<logger level="WARN" name="org.jooq"/>
|
||||||
|
</configuration>
|
||||||
Loading…
x
Reference in New Issue
Block a user