Compare commits

..

No commits in common. "90701dcdce20b71f11944c00ba6d06caa78fc738" and "e6a7af840ad283dc5842bd23fe23fb4154770d73" have entirely different histories.

156 changed files with 83 additions and 121 deletions

View File

@ -21,7 +21,7 @@ RUN chown -R $APPLICATION_USER /app
USER $APPLICATION_USER USER $APPLICATION_USER
COPY --from=jdkbuilder /myjdk /myjdk COPY --from=jdkbuilder /myjdk /myjdk
COPY app/build/libs/app-with-dependencies*.jar /app/simplenotes.jar COPY simplenotes-app/build/libs/simplenotes-app-with-dependencies*.jar /app/simplenotes.jar
WORKDIR /app WORKDIR /app
CMD [ \ CMD [ \

View File

@ -1 +0,0 @@
package be.simplenotes.app

View File

@ -15,15 +15,15 @@ open class CssTask : DefaultTask() {
private val viewsProject = project private val viewsProject = project
.parent .parent
?.project(":views") ?.project(":simplenotes-views")
?: error("Missing :views") ?: error("Missing :simplenotes-views")
@get:InputDirectory @get:InputDirectory
val templatesDir = viewsProject.extensions val templatesDir = viewsProject.extensions
.getByType<SourceSetContainer>() .getByType<SourceSetContainer>()
.asMap.getOrElse("main") { error("main sources not found") } .asMap.getOrElse("main") { error("main sources not found") }
.allSource.srcDirs .allSource.srcDirs
.find { it.endsWith("src") } .find { it.endsWith("kotlin") }
?: error("kotlin sources not found") ?: error("kotlin sources not found")
private val yarnRoot = File(project.rootDir, "css") private val yarnRoot = File(project.rootDir, "css")

View File

@ -24,6 +24,3 @@ java {
tasks.withType<JavaCompile> { tasks.withType<JavaCompile> {
options.encoding = "UTF-8" options.encoding = "UTF-8"
} }
sourceSets["main"].resources.srcDirs("resources")
sourceSets["test"].resources.srcDirs("testresources")

View File

@ -10,5 +10,5 @@ tasks.withType<Test> {
sourceSets { sourceSets {
val test by getting val test by getting
test.resources.srcDir("${rootProject.projectDir}/testresources/src/test/resources") test.resources.srcDir("${rootProject.projectDir}/simplenotes-test-resources/src/test/resources")
} }

View File

@ -23,6 +23,3 @@ tasks.withType<KotlinCompile> {
) )
} }
} }
kotlin.sourceSets["main"].kotlin.srcDirs("src")
kotlin.sourceSets["test"].kotlin.srcDirs("test")

View File

@ -1 +0,0 @@
package be.simplenotes.domain

View File

@ -1,30 +0,0 @@
package be.simplenotes.domain.security
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWTCreator
import com.auth0.jwt.interfaces.DecodedJWT
import javax.inject.Singleton
interface JwtMapper<T> {
fun extract(decodedJWT: DecodedJWT): T?
fun build(builder: JWTCreator.Builder, value: T)
}
@Singleton
class UserJwtMapper : JwtMapper<LoggedInUser> {
private val userIdField = "i"
private val usernameField = "u"
override fun extract(decodedJWT: DecodedJWT): LoggedInUser? {
val id = decodedJWT.getClaim(userIdField).asInt() ?: null
val username = decodedJWT.getClaim(usernameField).asString() ?: null
return if (id != null && username != null)
LoggedInUser(id, username)
else null
}
override fun build(builder: JWTCreator.Builder, value: LoggedInUser) {
builder.withClaim(userIdField, value.userId)
.withClaim(usernameField, value.username)
}
}

View File

@ -1 +0,0 @@
package be.simplenotes.domain

View File

@ -1,8 +1,8 @@
rootProject.name = "simplenotes" rootProject.name = "simplenotes"
include(":config") include(":simplenotes-config")
include(":views") include(":simplenotes-views")
include(":app") include(":simplenotes-app")
include(":domain") include(":simplenotes-domain")
include(":search") include(":simplenotes-search")
include(":types") include(":simplenotes-types")
include(":persistance") include(":simplenotes-persistance")

View File

@ -10,10 +10,10 @@ plugins {
} }
dependencies { dependencies {
implementation(project(":domain")) implementation(project(":simplenotes-domain"))
implementation(project(":types")) implementation(project(":simplenotes-types"))
implementation(project(":config")) implementation(project(":simplenotes-config"))
implementation(project(":views")) implementation(project(":simplenotes-views"))
implementation(Libs.arrowCoreData) implementation(Libs.arrowCoreData)
implementation(Libs.konform) implementation(Libs.konform)

View File

@ -1,14 +1,13 @@
package be.simplenotes.app.filters.auth package be.simplenotes.app.filters.auth
import be.simplenotes.app.filters.auth.JwtSource.Cookie import be.simplenotes.app.filters.auth.JwtSource.Cookie
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Filter import org.http4k.core.Filter
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import org.http4k.core.with import org.http4k.core.with
class OptionalAuthFilter( class OptionalAuthFilter(
private val simpleJwt: SimpleJwt<LoggedInUser>, private val extractor: JwtPayloadExtractor,
private val lens: OptionalAuthLens, private val lens: OptionalAuthLens,
private val source: JwtSource = Cookie, private val source: JwtSource = Cookie,
) : Filter { ) : Filter {
@ -18,6 +17,6 @@ class OptionalAuthFilter(
Cookie -> it.bearerTokenCookie() Cookie -> it.bearerTokenCookie()
} }
next(it.with(lens of token?.let { simpleJwt.extract(it) })) next(it.with(lens of token?.let { extractor(it) }))
} }
} }

View File

@ -1,8 +1,7 @@
package be.simplenotes.app.filters.auth package be.simplenotes.app.filters.auth
import be.simplenotes.app.extensions.redirect import be.simplenotes.app.extensions.redirect
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.types.LoggedInUser
import org.http4k.core.Filter import org.http4k.core.Filter
import org.http4k.core.HttpHandler import org.http4k.core.HttpHandler
import org.http4k.core.Response import org.http4k.core.Response
@ -10,7 +9,7 @@ import org.http4k.core.Status.Companion.UNAUTHORIZED
import org.http4k.core.with import org.http4k.core.with
class RequiredAuthFilter( class RequiredAuthFilter(
private val simpleJwt: SimpleJwt<LoggedInUser>, private val extractor: JwtPayloadExtractor,
private val lens: RequiredAuthLens, private val lens: RequiredAuthLens,
private val source: JwtSource = JwtSource.Cookie, private val source: JwtSource = JwtSource.Cookie,
private val redirect: Boolean = true, private val redirect: Boolean = true,
@ -20,7 +19,7 @@ class RequiredAuthFilter(
JwtSource.Header -> it.bearerTokenHeader() JwtSource.Header -> it.bearerTokenHeader()
JwtSource.Cookie -> it.bearerTokenCookie() JwtSource.Cookie -> it.bearerTokenCookie()
} }
val jwtPayload = token?.let { simpleJwt.extract(token) } val jwtPayload = token?.let { extractor(token) }
if (jwtPayload != null) next(it.with(lens of jwtPayload)) if (jwtPayload != null) next(it.with(lens of jwtPayload))
else { else {

View File

@ -1,8 +1,7 @@
package be.simplenotes.app.modules package be.simplenotes.app.modules
import be.simplenotes.app.filters.auth.* import be.simplenotes.app.filters.auth.*
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.JwtPayloadExtractor
import be.simplenotes.types.LoggedInUser
import io.micronaut.context.annotation.Factory import io.micronaut.context.annotation.Factory
import io.micronaut.context.annotation.Primary import io.micronaut.context.annotation.Primary
import org.http4k.core.RequestContexts import org.http4k.core.RequestContexts
@ -22,21 +21,21 @@ class AuthModule {
fun requiredAuthLens(ctx: RequestContexts): RequiredAuthLens = RequestContextKey.required(ctx) fun requiredAuthLens(ctx: RequestContexts): RequiredAuthLens = RequestContextKey.required(ctx)
@Singleton @Singleton
fun optionalAuth(simpleJwt: SimpleJwt<LoggedInUser>, @Named("optional") lens: OptionalAuthLens) = fun optionalAuth(extractor: JwtPayloadExtractor, @Named("optional") lens: OptionalAuthLens) =
OptionalAuthFilter(simpleJwt, lens) OptionalAuthFilter(extractor, lens)
@Primary @Primary
@Singleton @Singleton
fun requiredAuth(simpleJwt: SimpleJwt<LoggedInUser>, @Named("required") lens: RequiredAuthLens) = fun requiredAuth(extractor: JwtPayloadExtractor, @Named("required") lens: RequiredAuthLens) =
RequiredAuthFilter(simpleJwt, lens) RequiredAuthFilter(extractor, lens)
@Singleton @Singleton
@Named("api") @Named("api")
internal fun apiAuthFilter( internal fun apiAuthFilter(
simpleJwt: SimpleJwt<LoggedInUser>, jwtPayloadExtractor: JwtPayloadExtractor,
@Named("required") lens: RequiredAuthLens, @Named("required") lens: RequiredAuthLens,
) = RequiredAuthFilter( ) = RequiredAuthFilter(
simpleJwt = simpleJwt, extractor = jwtPayloadExtractor,
lens = lens, lens = lens,
source = JwtSource.Header, source = JwtSource.Header,
redirect = false redirect = false

View File

@ -14,5 +14,5 @@
<logger name="com.zaxxer.hikari" level="INFO"/> <logger name="com.zaxxer.hikari" level="INFO"/>
<logger name="org.flywaydb.core" level="INFO"/> <logger name="org.flywaydb.core" level="INFO"/>
<logger name="io.micronaut" level="INFO"/> <logger name="io.micronaut" level="INFO"/>
<logger name="io.micronaut.context.lifecycle" level="INFO"/> <logger name="io.micronaut.context.lifecycle" level="DEBUG"/>
</configuration> </configuration>

View File

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.8 KiB

View File

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

Before

Width:  |  Height:  |  Size: 6.2 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

View File

Before

Width:  |  Height:  |  Size: 814 B

After

Width:  |  Height:  |  Size: 814 B

View File

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View File

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

View File

@ -6,7 +6,6 @@ import be.simplenotes.app.filters.auth.RequiredAuthFilter
import be.simplenotes.app.filters.auth.RequiredAuthLens import be.simplenotes.app.filters.auth.RequiredAuthLens
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.security.UserJwtMapper
import be.simplenotes.types.LoggedInUser import be.simplenotes.types.LoggedInUser
import com.natpryce.hamkrest.assertion.assertThat import com.natpryce.hamkrest.assertion.assertThat
import io.micronaut.context.BeanContext import io.micronaut.context.BeanContext
@ -33,7 +32,7 @@ internal class RequiredAuthFilterTest {
// region setup // region setup
private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS) private val jwtConfig = JwtConfig("secret", 1, TimeUnit.HOURS)
private val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper()) private val simpleJwt = SimpleJwt(jwtConfig)
private val beanCtx = BeanContext.build() private val beanCtx = BeanContext.build()
.registerSingleton(jwtConfig) .registerSingleton(jwtConfig)

View File

@ -7,10 +7,10 @@ plugins {
} }
dependencies { dependencies {
implementation(project(":config")) implementation(project(":simplenotes-config"))
implementation(project(":types")) implementation(project(":simplenotes-types"))
implementation(project(":persistance")) implementation(project(":simplenotes-persistance"))
implementation(project(":search")) implementation(project(":simplenotes-search"))
implementation(Libs.micronaut) implementation(Libs.micronaut)
kapt(Libs.micronautProcessor) kapt(Libs.micronautProcessor)

View File

@ -0,0 +1,19 @@
package be.simplenotes.domain.security
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.exceptions.JWTVerificationException
import javax.inject.Singleton
@Singleton
class JwtPayloadExtractor(private val jwt: SimpleJwt) {
operator fun invoke(token: String): LoggedInUser? = 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) } }
} catch (e: JWTVerificationException) {
null
} catch (e: IllegalArgumentException) {
null
}
}

View File

@ -1,33 +1,28 @@
package be.simplenotes.domain.security package be.simplenotes.domain.security
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.types.LoggedInUser
import com.auth0.jwt.JWT import com.auth0.jwt.JWT
import com.auth0.jwt.JWTVerifier import com.auth0.jwt.JWTVerifier
import com.auth0.jwt.algorithms.Algorithm import com.auth0.jwt.algorithms.Algorithm
import com.auth0.jwt.exceptions.JWTVerificationException
import java.util.* import java.util.*
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import javax.inject.Singleton import javax.inject.Singleton
internal const val userIdField = "i"
internal const val usernameField = "u"
@Singleton @Singleton
class SimpleJwt<T>(jwtConfig: JwtConfig, private val mapper: JwtMapper<T>) { class SimpleJwt(jwtConfig: JwtConfig) {
private val validityInMs = TimeUnit.MILLISECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit) private val validityInMs = TimeUnit.MILLISECONDS.convert(jwtConfig.validity, jwtConfig.timeUnit)
private val algorithm = Algorithm.HMAC256(jwtConfig.secret) private val algorithm = Algorithm.HMAC256(jwtConfig.secret)
private val verifier: JWTVerifier = JWT.require(algorithm).build()
fun sign(value: T): String = JWT.create() val verifier: JWTVerifier = JWT.require(algorithm).build()
.apply { mapper.build(this, value) } fun sign(loggedInUser: LoggedInUser): String = JWT.create()
.withClaim(userIdField, loggedInUser.userId)
.withClaim(usernameField, loggedInUser.username)
.withExpiresAt(getExpiration()) .withExpiresAt(getExpiration())
.sign(algorithm) .sign(algorithm)
fun extract(token: String): T? = try {
val decodedJWT = verifier.verify(token)
mapper.extract(decodedJWT)
} catch (e: JWTVerificationException) {
null
} catch (e: IllegalArgumentException) {
null
}
private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs) private fun getExpiration() = Date(System.currentTimeMillis() + validityInMs)
} }

View File

@ -16,7 +16,7 @@ import javax.inject.Singleton
internal class LoginUseCaseImpl( internal class LoginUseCaseImpl(
private val userRepository: UserRepository, private val userRepository: UserRepository,
private val passwordHash: PasswordHash, private val passwordHash: PasswordHash,
private val jwt: SimpleJwt<LoggedInUser> private val jwt: SimpleJwt
) : LoginUseCase { ) : LoginUseCase {
override fun login(form: LoginForm) = either.eager<LoginError, Token> { override fun login(form: LoginForm) = either.eager<LoginError, Token> {
val user = !UserValidations.validateLogin(form) val user = !UserValidations.validateLogin(form)

View File

@ -16,13 +16,14 @@ import java.util.stream.Stream
internal class LoggedInUserExtractorTest { internal class LoggedInUserExtractorTest {
private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS) private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
private val mapper = UserJwtMapper() private val simpleJwt = SimpleJwt(jwtConfig)
private val simpleJwt = SimpleJwt(jwtConfig, mapper) private val jwtPayloadExtractor = JwtPayloadExtractor(simpleJwt)
private fun createToken(username: String? = null, id: Int? = null, secret: String = jwtConfig.secret): Token { private fun createToken(username: String? = null, id: Int? = null, secret: String = jwtConfig.secret): Token {
val algo = Algorithm.HMAC256(secret) val algo = Algorithm.HMAC256(secret)
return JWT.create().apply { return JWT.create().apply {
if (username != null && id != null) mapper.build(this, LoggedInUser(id, username)) username?.let { withClaim(usernameField, it) }
id?.let { withClaim(userIdField, it) }
}.sign(algo) }.sign(algo)
} }
@ -39,12 +40,12 @@ internal class LoggedInUserExtractorTest {
@ParameterizedTest(name = "[{index}] token `{0}` should be invalid") @ParameterizedTest(name = "[{index}] token `{0}` should be invalid")
@MethodSource("invalidTokens") @MethodSource("invalidTokens")
fun `parse invalid tokens`(token: String) { fun `parse invalid tokens`(token: String) {
assertThat(simpleJwt.extract(token), absent()) assertThat(jwtPayloadExtractor(token), absent())
} }
@Test @Test
fun `parse valid token`() { fun `parse valid token`() {
val token = createToken(username = "someone", id = 1) val token = createToken(username = "someone", id = 1)
assertThat(simpleJwt.extract(token), equalTo(LoggedInUser(1, "someone"))) assertThat(jwtPayloadExtractor(token), equalTo(LoggedInUser(1, "someone")))
} }
} }

View File

@ -3,7 +3,6 @@ package be.simplenotes.domain.usecases.users.login
import be.simplenotes.config.JwtConfig import be.simplenotes.config.JwtConfig
import be.simplenotes.domain.security.BcryptPasswordHash import be.simplenotes.domain.security.BcryptPasswordHash
import be.simplenotes.domain.security.SimpleJwt import be.simplenotes.domain.security.SimpleJwt
import be.simplenotes.domain.security.UserJwtMapper
import be.simplenotes.domain.testutils.isLeftOfType import be.simplenotes.domain.testutils.isLeftOfType
import be.simplenotes.domain.testutils.isRight import be.simplenotes.domain.testutils.isRight
import be.simplenotes.persistance.repositories.UserRepository import be.simplenotes.persistance.repositories.UserRepository
@ -19,7 +18,7 @@ internal class LoginUseCaseImplTest {
private val mockUserRepository = mockk<UserRepository>() private val mockUserRepository = mockk<UserRepository>()
private val passwordHash = BcryptPasswordHash(test = true) private val passwordHash = BcryptPasswordHash(test = true)
private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS) private val jwtConfig = JwtConfig("a secret", 1, TimeUnit.HOURS)
private val simpleJwt = SimpleJwt(jwtConfig, UserJwtMapper()) private val simpleJwt = SimpleJwt(jwtConfig)
private val loginUseCase = LoginUseCaseImpl(mockUserRepository, passwordHash, simpleJwt) private val loginUseCase = LoginUseCaseImpl(mockUserRepository, passwordHash, simpleJwt)
@BeforeEach @BeforeEach

View File

@ -7,8 +7,8 @@ plugins {
} }
dependencies { dependencies {
implementation(project(":types")) implementation(project(":simplenotes-types"))
implementation(project(":config")) implementation(project(":simplenotes-config"))
implementation(Libs.mariadbClient) implementation(Libs.mariadbClient)
implementation(Libs.h2) implementation(Libs.h2)
@ -29,9 +29,9 @@ dependencies {
testImplementation(Libs.logbackClassic) testImplementation(Libs.logbackClassic)
testImplementation(Libs.mariaTestContainer) testImplementation(Libs.mariaTestContainer)
testFixturesImplementation(project(":types")) testFixturesImplementation(project(":simplenotes-types"))
testFixturesImplementation(project(":config")) testFixturesImplementation(project(":simplenotes-config"))
testFixturesImplementation(project(":persistance")) testFixturesImplementation(project(":simplenotes-persistance"))
testFixturesImplementation(Libs.micronaut) testFixturesImplementation(Libs.micronaut)
kaptTestFixtures(Libs.micronautProcessor) kaptTestFixtures(Libs.micronautProcessor)
@ -48,5 +48,3 @@ dependencies {
testFixturesImplementation(Libs.ktormCore) testFixturesImplementation(Libs.ktormCore)
testFixturesImplementation(Libs.hikariCP) testFixturesImplementation(Libs.hikariCP)
} }
kotlin.sourceSets["testFixtures"].kotlin.srcDirs("testfixtures")

View File

@ -1,7 +1,6 @@
package be.simplenotes.persistance.extensions package be.simplenotes.persistance.extensions
import me.liuwj.ktorm.schema.BaseTable import me.liuwj.ktorm.schema.*
import me.liuwj.ktorm.schema.SqlType
import java.nio.ByteBuffer import java.nio.ByteBuffer
import java.sql.PreparedStatement import java.sql.PreparedStatement
import java.sql.ResultSet import java.sql.ResultSet

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