refactoring: Masive refactor to build the V2
This commit is contained in:
+24
@@ -0,0 +1,24 @@
|
||||
package eventDemo.contexts.auth.application.eventStores
|
||||
|
||||
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||
import eventDemo.contexts.auth.domain.User
|
||||
import eventDemo.sharedKernel.UserId
|
||||
|
||||
class UserEventStoreRepository(
|
||||
val eventStore: UserEventStore,
|
||||
) : UserRepository {
|
||||
override fun get(id: UserId): User? {
|
||||
val events =
|
||||
eventStore
|
||||
.getStream(id)
|
||||
.readAll()
|
||||
if (events.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
return events.let { User.loadFromHistory(it) }
|
||||
}
|
||||
|
||||
override fun save(user: User) {
|
||||
eventStore.publish(user.recordedEvents)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package eventDemo.contexts.auth.application.eventStores
|
||||
|
||||
import eventDemo.contexts.auth.domain.User
|
||||
import eventDemo.sharedKernel.UserId
|
||||
|
||||
interface UserRepository {
|
||||
fun get(id: UserId): User?
|
||||
|
||||
fun save(user: User)
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package eventDemo.contexts.auth.application.ports
|
||||
|
||||
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||
import eventDemo.sharedKernel.UserId
|
||||
|
||||
interface UserEventStore : EventStore<UserEvent, UserId>
|
||||
@@ -0,0 +1,14 @@
|
||||
package eventDemo.contexts.auth.application.ports
|
||||
|
||||
import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjection
|
||||
|
||||
interface UserProjectionRepository {
|
||||
fun getByUsername(username: String): UserProjection?
|
||||
|
||||
fun save(user: UserProjection)
|
||||
|
||||
fun getUserIfPasswordIsValid(
|
||||
username: String,
|
||||
rawPassword: String,
|
||||
): UserProjection?
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package eventDemo.contexts.auth.domain
|
||||
|
||||
import eventDemo.contexts.auth.domain.events.NewUserCreatedEvent
|
||||
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||
import eventDemo.sharedKernel.UserId
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class User(
|
||||
val id: UserId,
|
||||
val username: String,
|
||||
val password: String,
|
||||
val version: Int,
|
||||
) {
|
||||
val recordedEvents: Set<UserEvent> = emptySet()
|
||||
|
||||
companion object {
|
||||
fun createNewUser(
|
||||
username: String,
|
||||
password: String,
|
||||
): User =
|
||||
apply(NewUserCreatedEvent(username, password, version = 1))
|
||||
|
||||
fun apply(event: NewUserCreatedEvent): User =
|
||||
User(
|
||||
id = event.aggregateId,
|
||||
username = event.username,
|
||||
password = event.password,
|
||||
version = event.version,
|
||||
)
|
||||
|
||||
fun loadFromHistory(events: Set<UserEvent>): User? =
|
||||
events.fold(null as User?) { acc, event ->
|
||||
when (event) {
|
||||
is NewUserCreatedEvent -> apply(event)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package eventDemo.contexts.auth.domain.events
|
||||
|
||||
import eventDemo.libs.eventSource.EventId
|
||||
import eventDemo.sharedKernel.UserId
|
||||
import kotlinx.datetime.Clock
|
||||
import kotlinx.datetime.Instant
|
||||
|
||||
class NewUserCreatedEvent(
|
||||
val username: String,
|
||||
val password: String,
|
||||
override val version: Int,
|
||||
override val createdAt: Instant = Clock.System.now(),
|
||||
override val aggregateId: UserId = UserId(),
|
||||
) : UserEvent {
|
||||
override val eventId: EventId = EventId()
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package eventDemo.contexts.auth.domain.events
|
||||
|
||||
import eventDemo.libs.eventSource.Event
|
||||
import eventDemo.sharedKernel.UserId
|
||||
|
||||
sealed interface UserEvent : Event<UserId>
|
||||
@@ -0,0 +1,13 @@
|
||||
package eventDemo.contexts.auth.infrastructure
|
||||
|
||||
import com.password4j.Hash
|
||||
import com.password4j.Password
|
||||
|
||||
internal fun hashPassword(password: String): Hash =
|
||||
Password.hash(password).addRandomSalt().withArgon2()
|
||||
|
||||
internal fun checkPassword(
|
||||
password: String,
|
||||
hash: Hash,
|
||||
): Boolean =
|
||||
Password.check(password, hash)
|
||||
@@ -0,0 +1,8 @@
|
||||
package eventDemo.contexts.auth.infrastructure.configure
|
||||
|
||||
import io.ktor.server.application.Application
|
||||
|
||||
fun Application.configureAuth() {
|
||||
configureKtorAuth()
|
||||
configureAuthRoutes()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package eventDemo.contexts.auth.infrastructure.configure
|
||||
|
||||
import eventDemo.contexts.auth.application.eventStores.UserEventStoreRepository
|
||||
import eventDemo.contexts.auth.application.eventStores.UserRepository
|
||||
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||
import eventDemo.contexts.auth.infrastructure.persistence.eventStore.UserEventStoreInPostgresql
|
||||
import eventDemo.contexts.auth.infrastructure.persistence.projection.UsedProjectionRepositoryInPostgresql
|
||||
import org.koin.core.module.Module
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.bind
|
||||
|
||||
fun Module.configureAuthDi() {
|
||||
singleOf(::UserEventStoreRepository) bind UserRepository::class
|
||||
singleOf(::UserEventStoreInPostgresql) bind UserEventStore::class
|
||||
singleOf(::UsedProjectionRepositoryInPostgresql) bind UserProjectionRepository::class
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
package eventDemo.contexts.auth.infrastructure.configure
|
||||
|
||||
import eventDemo.configuration.configuration
|
||||
import eventDemo.contexts.auth.application.eventStores.UserEventStoreRepository
|
||||
import eventDemo.contexts.auth.application.eventStores.UserRepository
|
||||
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||
import eventDemo.contexts.auth.infrastructure.rest.createUserRoute
|
||||
import eventDemo.contexts.auth.infrastructure.rest.loginRoute
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.routing.routing
|
||||
import org.koin.ktor.ext.get
|
||||
|
||||
fun Application.configureAuthRoutes() {
|
||||
val userRepository = get<UserEventStoreRepository>()
|
||||
val userProjectionRepository = get<UserProjectionRepository>()
|
||||
routing {
|
||||
createUserRoute(userRepository)
|
||||
loginRoute(environment.config.configuration.jwtSecret, userProjectionRepository)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package eventDemo.contexts.auth.infrastructure.configure
|
||||
|
||||
import com.auth0.jwt.JWT
|
||||
import com.auth0.jwt.algorithms.Algorithm
|
||||
import eventDemo.configuration.configuration
|
||||
import eventDemo.contexts.auth.domain.User
|
||||
import eventDemo.contexts.auth.infrastructure.persistence.projection.UserProjection
|
||||
import eventDemo.sharedKernel.UserId
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.auth.authentication
|
||||
import io.ktor.server.auth.jwt.JWTPrincipal
|
||||
import io.ktor.server.auth.jwt.jwt
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.routing
|
||||
import kotlinx.serialization.json.Json
|
||||
import java.util.Date
|
||||
import java.util.UUID
|
||||
|
||||
fun Application.configureKtorAuth() {
|
||||
val jwtSecret = environment.config.configuration.jwtSecret
|
||||
authentication {
|
||||
jwt {
|
||||
realm = "Play card game"
|
||||
verifier(
|
||||
JWT
|
||||
.require(Algorithm.HMAC256(jwtSecret))
|
||||
.withIssuer(JWT_ISSUER)
|
||||
.build(),
|
||||
)
|
||||
validate { credential ->
|
||||
if (credential.payload
|
||||
.getClaim("username")
|
||||
.asString()
|
||||
.isNotEmpty()
|
||||
) {
|
||||
JWTPrincipal(credential.payload)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
challenge { _, _ ->
|
||||
call.respond(HttpStatusCode.Unauthorized, "Token is not valid or has expired")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private const val JWT_ISSUER = "PlayCardGame"
|
||||
|
||||
fun UserProjection.makeJwt(jwtSecret: String): String =
|
||||
makeJwt(jwtSecret, id, username)
|
||||
|
||||
fun User.makeJwt(jwtSecret: String): String =
|
||||
makeJwt(jwtSecret, id, username)
|
||||
|
||||
fun makeJwt(
|
||||
jwtSecret: String,
|
||||
id: UserId,
|
||||
username: String,
|
||||
): String =
|
||||
JWT
|
||||
.create()
|
||||
.withIssuer(JWT_ISSUER)
|
||||
.withClaim("username", username)
|
||||
.withClaim("userid", id.toString())
|
||||
.withExpiresAt(Date(System.currentTimeMillis() + 60000))
|
||||
.sign(Algorithm.HMAC256(jwtSecret))
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package eventDemo.contexts.auth.infrastructure.persistence.eventStore
|
||||
|
||||
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||
import eventDemo.libs.eventSource.eventStore.EventStoreInMemory
|
||||
import eventDemo.sharedKernel.UserId
|
||||
|
||||
/**
|
||||
* A stream to publish and read the user events.
|
||||
*/
|
||||
class UserEventStoreInMemory :
|
||||
UserEventStore,
|
||||
EventStore<UserEvent, UserId> by EventStoreInMemory()
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package eventDemo.contexts.auth.infrastructure.persistence.eventStore
|
||||
|
||||
import eventDemo.contexts.auth.application.ports.UserEventStore
|
||||
import eventDemo.contexts.auth.domain.events.UserEvent
|
||||
import eventDemo.libs.eventSource.eventStore.EventStore
|
||||
import eventDemo.libs.eventSource.eventStore.EventStoreInPostgresql
|
||||
import eventDemo.sharedKernel.UserId
|
||||
import kotlinx.serialization.json.Json
|
||||
import javax.sql.DataSource
|
||||
|
||||
/**
|
||||
* A stream to publish and read the user events.
|
||||
*/
|
||||
class UserEventStoreInPostgresql(
|
||||
dataSource: DataSource,
|
||||
) : UserEventStore,
|
||||
EventStore<UserEvent, UserId> by EventStoreInPostgresql(
|
||||
dataSource,
|
||||
{ Json.encodeToString(it) },
|
||||
{ Json.decodeFromString(it) },
|
||||
"auth.user_event_store",
|
||||
)
|
||||
+61
@@ -0,0 +1,61 @@
|
||||
package eventDemo.contexts.auth.infrastructure.persistence.projection
|
||||
|
||||
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||
import eventDemo.contexts.auth.infrastructure.checkPassword
|
||||
import eventDemo.contexts.auth.infrastructure.hashPassword
|
||||
import eventDemo.sharedKernel.UserId
|
||||
import java.util.UUID
|
||||
import javax.sql.DataSource
|
||||
|
||||
class UsedProjectionRepositoryInPostgresql(
|
||||
val dataSource: DataSource,
|
||||
) : UserProjectionRepository {
|
||||
override fun getByUsername(username: String): UserProjection? =
|
||||
dataSource.connection
|
||||
.prepareStatement(
|
||||
"""
|
||||
select id, username
|
||||
from auth."user"
|
||||
where id = ?;
|
||||
""".trimIndent(),
|
||||
).use {
|
||||
it.setObject(1, username)
|
||||
it.executeQuery()
|
||||
}.use { resultSet ->
|
||||
if (resultSet.next()) {
|
||||
UserProjection(
|
||||
id = UserId(UUID.fromString(resultSet.getString("id"))),
|
||||
username = resultSet.getString("username"),
|
||||
password = resultSet.getString("password"),
|
||||
)
|
||||
} else {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
override fun save(user: UserProjection) {
|
||||
dataSource.connection.use { connection ->
|
||||
connection
|
||||
.prepareStatement(
|
||||
"""
|
||||
insert into auth.user (id, username)
|
||||
values (?, ?)
|
||||
""".trimIndent(),
|
||||
).use {
|
||||
it.setObject(1, user.id)
|
||||
it.setString(2, user.username)
|
||||
it.executeUpdate()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun getUserIfPasswordIsValid(
|
||||
username: String,
|
||||
rawPassword: String,
|
||||
): UserProjection? {
|
||||
val user = getByUsername(username) ?: return null
|
||||
val isValid = checkPassword(rawPassword, hashPassword(user.password))
|
||||
if (!isValid) return null
|
||||
return user
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package eventDemo.contexts.auth.infrastructure.persistence.projection
|
||||
|
||||
import eventDemo.sharedKernel.UserId
|
||||
|
||||
data class UserProjection(
|
||||
val id: UserId,
|
||||
val username: String,
|
||||
val password: String,
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package eventDemo.contexts.auth.infrastructure.rest
|
||||
|
||||
import eventDemo.contexts.auth.application.ports.UserProjectionRepository
|
||||
import eventDemo.contexts.auth.infrastructure.configure.makeJwt
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.post
|
||||
|
||||
fun Route.loginRoute(
|
||||
jwtSecret: String,
|
||||
userProjectionRepository: UserProjectionRepository,
|
||||
) {
|
||||
post("login/{username}") {
|
||||
val username = call.parameters["username"]!!
|
||||
val rawPassword = call.parameters["password"]!!
|
||||
|
||||
val userProjection =
|
||||
userProjectionRepository.getUserIfPasswordIsValid(username, rawPassword)
|
||||
?: return@post call.respond(HttpStatusCode.BadRequest)
|
||||
|
||||
call.respond(hashMapOf("token" to userProjection.makeJwt(jwtSecret)))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package eventDemo.contexts.auth.infrastructure.rest
|
||||
|
||||
import com.password4j.Hash
|
||||
import com.password4j.Password
|
||||
import eventDemo.contexts.auth.application.eventStores.UserRepository
|
||||
import eventDemo.contexts.auth.domain.User
|
||||
import eventDemo.contexts.auth.infrastructure.hashPassword
|
||||
import io.ktor.resources.Resource
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.post
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
@Resource("/users")
|
||||
class Users {
|
||||
@Serializable
|
||||
@Resource("/create")
|
||||
class Create(
|
||||
val username: String,
|
||||
val password: String,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* API routes to show all games.
|
||||
*/
|
||||
fun Route.createUserRoute(userRepository: UserRepository) {
|
||||
authenticate {
|
||||
// Create a new User, and return there ID
|
||||
post<Users.Create> {
|
||||
val passwordHash = hashPassword(it.password)
|
||||
val user = User.createNewUser(it.username, passwordHash.result)
|
||||
userRepository.save(user)
|
||||
call.respond(
|
||||
object {
|
||||
val id = user.id.toString()
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user