This commit is contained in:
2025-08-20 00:15:06 +02:00
parent 7beeb39616
commit b868e5c675
22 changed files with 93 additions and 228 deletions

View File

@@ -29,9 +29,8 @@ dependencies {
implementation(libs.exposed) implementation(libs.exposed)
implementation(libs.exposed.dao) implementation(libs.exposed.dao)
implementation(libs.exposed.jdbc) implementation(libs.exposed.jdbc)
implementation(libs.migration)
implementation(libs.postgres) implementation(libs.postgres)
implementation(libs.koin)
implementation(libs.koin.logger)
implementation(libs.random) implementation(libs.random)
implementation(libs.otp) implementation(libs.otp)
implementation(libs.kdf) implementation(libs.kdf)

View File

@@ -2,8 +2,8 @@
kotlin = "2.2.10" kotlin = "2.2.10"
ktor = "3.2.3" ktor = "3.2.3"
exposed = "0.60.0" exposed = "0.60.0"
migration = "1.0.0"
postgres = "42.7.5" postgres = "42.7.5"
koin = "4.1.0"
random = "1.0.0" random = "1.0.0"
otp = "1.1.1" otp = "1.1.1"
kdf = "1.0.3" kdf = "1.0.3"
@@ -30,11 +30,9 @@ ktor-server-validation = { module = "io.ktor:ktor-server-request-validation", ve
exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" } exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "exposed" }
exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" } exposed-dao = { module = "org.jetbrains.exposed:exposed-dao", version.ref = "exposed" }
exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" } exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "exposed" }
migration = { module = "com.infendro:ktor-migration", version.ref = "migration" }
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" } postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
koin = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
koin-logger = { module = "io.insert-koin:koin-logger-slf4j", version.ref = "koin" }
random = { module = "com.infendro:random", version.ref = "random" } random = { module = "com.infendro:random", version.ref = "random" }
otp = { module = "com.infendro:otp", version.ref = "otp" } otp = { module = "com.infendro:otp", version.ref = "otp" }
kdf = { module = "com.infendro:kdf", version.ref = "kdf" } kdf = { module = "com.infendro:kdf", version.ref = "kdf" }

View File

@@ -9,8 +9,6 @@ fun main(
) = EngineMain.main(args) ) = EngineMain.main(args)
fun Application.module() { fun Application.module() {
configureDependencyInjection()
configureDatabase() configureDatabase()
configureSerialization() configureSerialization()

View File

@@ -1,15 +1,11 @@
package com.infendro.account.config package com.infendro.account.config
import com.infendro.account.model.Role import com.infendro.account.model.Role
import com.infendro.account.model.entity.MigrationTable
import com.infendro.account.model.migration
import com.infendro.account.model.repository.AccountRepository import com.infendro.account.model.repository.AccountRepository
import com.infendro.account.util.SecureHasher import com.infendro.account.util.SecureHasher
import com.infendro.migration.Migration
import io.ktor.server.application.* import io.ktor.server.application.*
import org.jetbrains.exposed.sql.Database import org.jetbrains.exposed.sql.Database
import org.jetbrains.exposed.sql.SchemaUtils
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.ktor.ext.inject
fun Application.configureDatabase() { fun Application.configureDatabase() {
val config = environment.config.database val config = environment.config.database
@@ -19,16 +15,8 @@ fun Application.configureDatabase() {
user = config.username, user = config.username,
password = config.password, password = config.password,
) )
migrate()
}
fun Application.migrate() {
transaction {
SchemaUtils.create(MigrationTable)
}
val accounts by inject<AccountRepository>()
install(Migration) {
migration("initial") { migration("initial") {
exec( exec(
""" """
@@ -70,7 +58,7 @@ fun Application.migrate() {
""".trimIndent() """.trimIndent()
) )
accounts.insert { AccountRepository.insert {
val salt = SecureHasher.generateSalt() val salt = SecureHasher.generateSalt()
this.username = "infendro" this.username = "infendro"
@@ -81,3 +69,4 @@ fun Application.migrate() {
} }
} }
} }
}

View File

@@ -1,34 +0,0 @@
package com.infendro.account.config
import com.infendro.account.model.repository.AccessRepository
import com.infendro.account.model.repository.AccountRepository
import com.infendro.account.model.repository.MigrationRepository
import com.infendro.account.model.repository.SessionRepository
import com.infendro.account.service.AccessService
import com.infendro.account.service.AccountService
import com.infendro.account.service.RoleService
import com.infendro.account.service.SessionService
import io.ktor.server.application.*
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
import org.koin.ktor.plugin.Koin
import org.koin.logger.slf4jLogger
fun Application.configureDependencyInjection() {
install(Koin) {
slf4jLogger()
modules(module)
}
}
private val module = module {
singleOf(::MigrationRepository)
singleOf(::AccessRepository)
singleOf(::AccountRepository)
singleOf(::SessionRepository)
singleOf(::AccessService)
singleOf(::AccountService)
singleOf(::RoleService)
singleOf(::SessionService)
}

View File

@@ -7,15 +7,12 @@ import com.infendro.account.model.entity.SessionEntity
import com.infendro.account.model.entity.SessionTable import com.infendro.account.model.entity.SessionTable
import com.infendro.account.model.repository.SessionRepository import com.infendro.account.model.repository.SessionRepository
import com.infendro.account.util.Hasher import com.infendro.account.util.Hasher
import io.ktor.server.application.Application import io.ktor.server.application.*
import io.ktor.server.application.install import io.ktor.server.auth.*
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.session
import io.ktor.server.sessions.* import io.ktor.server.sessions.*
import kotlin.time.Duration.Companion.days
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.koin.ktor.ext.inject import kotlin.time.Duration.Companion.days
@Serializable @Serializable
data class AuthenticationSession( data class AuthenticationSession(
@@ -46,9 +43,7 @@ fun Application.configureSecurity() {
install(Authentication) { install(Authentication) {
session<AuthenticationSession> { session<AuthenticationSession> {
validate { authenticationSession -> validate { authenticationSession ->
val sessionRepository by inject<SessionRepository>() val session = SessionRepository
val session = sessionRepository
.singleOrNull { SessionTable.tokenHash eq Hasher.hash(authenticationSession.token) } .singleOrNull { SessionTable.tokenHash eq Hasher.hash(authenticationSession.token) }
?: return@validate null ?: return@validate null

View File

@@ -1,34 +0,0 @@
package com.infendro.account.model
import com.infendro.account.model.entity.MigrationTable
import com.infendro.account.model.repository.MigrationRepository
import io.ktor.server.application.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.Transaction
import org.jetbrains.exposed.sql.transactions.transaction
import org.koin.ktor.ext.inject
fun Application.migration(
name: String,
block: Transaction.() -> Unit,
) {
val migrations by inject<MigrationRepository>()
migrations
.singleOrNull { MigrationTable.name eq name }
?.run { return }
transaction {
try {
block()
log.info("""Migration "$name" successful""")
} catch (e: Exception) {
log.error("""Migration "$name" failed""")
throw e
}
}
migrations.insert {
this.name = name
}
}

View File

@@ -1,16 +0,0 @@
package com.infendro.account.model.entity
import org.jetbrains.exposed.dao.LongEntity
import org.jetbrains.exposed.dao.LongEntityClass
import org.jetbrains.exposed.dao.id.EntityID
import org.jetbrains.exposed.dao.id.LongIdTable
object MigrationTable : LongIdTable("migration", "id") {
val name = text("name")
}
class MigrationEntity(id: EntityID<Long>) : LongEntity(id) {
companion object : LongEntityClass<MigrationEntity>(MigrationTable)
var name by MigrationTable.name
}

View File

@@ -2,4 +2,4 @@ package com.infendro.account.model.repository
import com.infendro.account.model.entity.AccessEntity import com.infendro.account.model.entity.AccessEntity
class AccessRepository : Repository<AccessEntity>(AccessEntity) object AccessRepository : Repository<AccessEntity>(AccessEntity)

View File

@@ -2,4 +2,4 @@ package com.infendro.account.model.repository
import com.infendro.account.model.entity.AccountEntity import com.infendro.account.model.entity.AccountEntity
class AccountRepository : Repository<AccountEntity>(AccountEntity) object AccountRepository : Repository<AccountEntity>(AccountEntity)

View File

@@ -1,5 +0,0 @@
package com.infendro.account.model.repository
import com.infendro.account.model.entity.MigrationEntity
class MigrationRepository : Repository<MigrationEntity>(MigrationEntity)

View File

@@ -50,8 +50,8 @@ abstract class Repository<ENTITY : LongEntity>(
fun exists( fun exists(
where: Op<Boolean>, where: Op<Boolean>,
): Boolean { ): Boolean = transaction {
return all(where).isNotEmpty() entityClass.find(where).any()
} }
fun exists( fun exists(

View File

@@ -2,4 +2,4 @@ package com.infendro.account.model.repository
import com.infendro.account.model.entity.SessionEntity import com.infendro.account.model.entity.SessionEntity
class SessionRepository : Repository<SessionEntity>(SessionEntity) object SessionRepository : Repository<SessionEntity>(SessionEntity)

View File

@@ -3,15 +3,12 @@ package com.infendro.account.routing
import com.infendro.account.config.AuthenticationPrincipal import com.infendro.account.config.AuthenticationPrincipal
import com.infendro.account.service.AccessService import com.infendro.account.service.AccessService
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import io.ktor.resources.Resource import io.ktor.resources.*
import io.ktor.server.auth.authenticate import io.ktor.server.auth.*
import io.ktor.server.auth.principal import io.ktor.server.resources.*
import io.ktor.server.resources.delete
import io.ktor.server.resources.get
import io.ktor.server.resources.post import io.ktor.server.resources.post
import io.ktor.server.response.respond import io.ktor.server.response.*
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import org.koin.ktor.ext.inject
@Resource("/access") @Resource("/access")
private class Access { private class Access {
@@ -28,20 +25,18 @@ private class Access {
} }
fun Routing.access() { fun Routing.access() {
val accessService by inject<AccessService>()
authenticate { authenticate {
post<Access> { post<Access> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
accessService.post(principal) AccessService.post(principal)
.also { call.respond(it) } .also { call.respond(it) }
} }
get<Access.All> { get<Access.All> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
accessService.getAll(principal) AccessService.getAll(principal)
.also { call.respond(it) } .also { call.respond(it) }
} }
@@ -49,7 +44,7 @@ fun Routing.access() {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
val id = resource.id val id = resource.id
accessService.deleteId(principal, id) AccessService.deleteId(principal, id)
call.respond(OK) call.respond(OK)
} }
} }

View File

@@ -13,7 +13,6 @@ import io.ktor.server.resources.put
import io.ktor.server.response.* import io.ktor.server.response.*
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.sessions.* import io.ktor.server.sessions.*
import org.koin.ktor.ext.inject
@Resource("/account") @Resource("/account")
private class Account { private class Account {
@@ -71,12 +70,10 @@ private class Account {
} }
fun Routing.account() { fun Routing.account() {
val accountService by inject<AccountService>()
post<Account> { post<Account> {
val request = call.receive<PostAccountRequest>() val request = call.receive<PostAccountRequest>()
accountService.post(request) AccountService.post(request)
.also { call.respond(it) } .also { call.respond(it) }
} }
@@ -84,21 +81,21 @@ fun Routing.account() {
get<Account.All> { get<Account.All> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
accountService.getAll(principal) AccountService.getAll(principal)
.also { call.respond(it) } .also { call.respond(it) }
} }
get<Account.Current> { get<Account.Current> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
accountService.getCurrent(principal) AccountService.getCurrent(principal)
.also { call.respond(it) } .also { call.respond(it) }
} }
delete<Account.Current> { delete<Account.Current> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
accountService.deleteCurrent(principal) AccountService.deleteCurrent(principal)
call.respond(OK) call.respond(OK)
} }
@@ -106,7 +103,7 @@ fun Routing.account() {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
val request = call.receive<PutAccountCurrentUsernameRequest>() val request = call.receive<PutAccountCurrentUsernameRequest>()
accountService.putCurrentUsername(principal, call.sessions, request) AccountService.putCurrentUsername(principal, call.sessions, request)
call.respond(OK) call.respond(OK)
} }
@@ -114,7 +111,7 @@ fun Routing.account() {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
val request = call.receive<PutAccountCurrentPasswordRequest>() val request = call.receive<PutAccountCurrentPasswordRequest>()
accountService.putCurrentPassword(principal, call.sessions, request) AccountService.putCurrentPassword(principal, call.sessions, request)
call.respond(OK) call.respond(OK)
} }
@@ -122,14 +119,14 @@ fun Routing.account() {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
val request = call.receive<PostAccountCurrentSecretRequest>() val request = call.receive<PostAccountCurrentSecretRequest>()
accountService.postCurrentSecret(principal, call.sessions, request) AccountService.postCurrentSecret(principal, call.sessions, request)
.also { call.respond(it) } .also { call.respond(it) }
} }
get<Account.Current.Session.All> { get<Account.Current.Session.All> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
accountService.getCurrentSessionAll(principal) AccountService.getCurrentSessionAll(principal)
.also { call.respond(it) } .also { call.respond(it) }
} }
@@ -137,7 +134,7 @@ fun Routing.account() {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
val id = resource.id val id = resource.id
accountService.deleteCurrentSessionId(principal, call.sessions, id) AccountService.deleteCurrentSessionId(principal, call.sessions, id)
call.respond(OK) call.respond(OK)
} }
@@ -145,7 +142,7 @@ fun Routing.account() {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
val id = resource.id val id = resource.id
accountService.deleteId(principal, id) AccountService.deleteId(principal, id)
call.respond(OK) call.respond(OK)
} }
@@ -154,7 +151,7 @@ fun Routing.account() {
val id = resource.parent.id val id = resource.parent.id
val request = call.receive<PutAccountIdRoleRequest>() val request = call.receive<PutAccountIdRoleRequest>()
accountService.putIdRole(principal, id, request) AccountService.putIdRole(principal, id, request)
call.respond(OK) call.respond(OK)
} }
} }

View File

@@ -1,11 +1,10 @@
package com.infendro.account.routing package com.infendro.account.routing
import com.infendro.account.service.RoleService import com.infendro.account.service.RoleService
import io.ktor.resources.Resource import io.ktor.resources.*
import io.ktor.server.resources.get import io.ktor.server.resources.*
import io.ktor.server.response.respond import io.ktor.server.response.*
import io.ktor.server.routing.Routing import io.ktor.server.routing.*
import org.koin.ktor.ext.inject
@Resource("/role") @Resource("/role")
private class Role { private class Role {
@@ -16,10 +15,8 @@ private class Role {
} }
fun Routing.role() { fun Routing.role() {
val roleService by inject<RoleService>()
get<Role.All> { get<Role.All> {
roleService.getAll() RoleService.getAll()
.also { call.respond(it) } .also { call.respond(it) }
} }
} }

View File

@@ -4,17 +4,14 @@ import com.infendro.account.config.AuthenticationPrincipal
import com.infendro.account.dto.request.PostSessionRequest import com.infendro.account.dto.request.PostSessionRequest
import com.infendro.account.service.SessionService import com.infendro.account.service.SessionService
import io.ktor.http.HttpStatusCode.Companion.OK import io.ktor.http.HttpStatusCode.Companion.OK
import io.ktor.resources.Resource import io.ktor.resources.*
import io.ktor.server.auth.authenticate import io.ktor.server.auth.*
import io.ktor.server.auth.principal import io.ktor.server.request.*
import io.ktor.server.request.receive import io.ktor.server.resources.*
import io.ktor.server.resources.delete
import io.ktor.server.resources.get
import io.ktor.server.resources.post import io.ktor.server.resources.post
import io.ktor.server.response.respond import io.ktor.server.response.*
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.sessions.sessions import io.ktor.server.sessions.*
import org.koin.ktor.ext.inject
@Resource("/session") @Resource("/session")
private class Session { private class Session {
@@ -36,12 +33,10 @@ private class Session {
} }
fun Routing.session() { fun Routing.session() {
val sessionService by inject<SessionService>()
post<Session> { post<Session> {
val request = call.receive<PostSessionRequest>() val request = call.receive<PostSessionRequest>()
sessionService.post(call.sessions, request) SessionService.post(call.sessions, request)
call.respond(OK) call.respond(OK)
} }
@@ -49,7 +44,7 @@ fun Routing.session() {
get<Session.All> { get<Session.All> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
sessionService.getAll(principal) SessionService.getAll(principal)
.also { call.respond(it) } .also { call.respond(it) }
} }
@@ -57,21 +52,21 @@ fun Routing.session() {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
val id = resource.id val id = resource.id
sessionService.deleteId(principal, id) SessionService.deleteId(principal, id)
call.respond(OK) call.respond(OK)
} }
get<Session.Current> { get<Session.Current> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
sessionService.getCurrent(principal) SessionService.getCurrent(principal)
.also { call.respond(it) } .also { call.respond(it) }
} }
delete<Session.Current> { delete<Session.Current> {
val principal = call.principal<AuthenticationPrincipal>()!! val principal = call.principal<AuthenticationPrincipal>()!!
sessionService.deleteCurrent(principal, call.sessions) SessionService.deleteCurrent(principal, call.sessions)
call.respond(OK) call.respond(OK)
} }
} }

View File

@@ -13,9 +13,7 @@ import com.infendro.account.util.Hasher
import com.infendro.account.util.TokenGenerator import com.infendro.account.util.TokenGenerator
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
class AccessService( object AccessService {
private val accessRepository: AccessRepository,
) {
fun post( fun post(
principal: AuthenticationPrincipal, principal: AuthenticationPrincipal,
): PostAccessResponse { ): PostAccessResponse {
@@ -23,7 +21,7 @@ class AccessService(
throw ForbiddenException() throw ForbiddenException()
val token = TokenGenerator.generate() val token = TokenGenerator.generate()
accessRepository.insert { AccessRepository.insert {
this.tokenHash = Hasher.hash(token) this.tokenHash = Hasher.hash(token)
} }
@@ -38,7 +36,7 @@ class AccessService(
if (principal.role != OWNER) if (principal.role != OWNER)
throw ForbiddenException() throw ForbiddenException()
return accessRepository return AccessRepository
.all() .all()
.map { it.toResponse() } .map { it.toResponse() }
} }
@@ -50,10 +48,10 @@ class AccessService(
if (principal.role != OWNER) if (principal.role != OWNER)
throw ForbiddenException() throw ForbiddenException()
val access = accessRepository val access = AccessRepository
.singleOrNull { AccessTable.id eq id } .singleOrNull { AccessTable.id eq id }
?: throw NotFoundException() ?: throw NotFoundException()
accessRepository.delete(access) AccessRepository.delete(access)
} }
} }

View File

@@ -20,26 +20,22 @@ import com.infendro.account.util.SecureHasher
import io.ktor.server.sessions.* import io.ktor.server.sessions.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
class AccountService( object AccountService {
private val accessRepository: AccessRepository,
private val accountRepository: AccountRepository,
private val sessionRepository: SessionRepository,
) {
fun post( fun post(
request: PostAccountRequest, request: PostAccountRequest,
): PostAccountResponse { ): PostAccountResponse {
val access = accessRepository val access = AccessRepository
.singleOrNull { AccessTable.tokenHash eq Hasher.hash(request.access.token) } .singleOrNull { AccessTable.tokenHash eq Hasher.hash(request.access.token) }
?: throw UnauthorizedException() ?: throw UnauthorizedException()
accountRepository AccountRepository
.singleOrNull { AccountTable.username eq request.username } .singleOrNull { AccountTable.username eq request.username }
?.run { throw ConflictException() } ?.run { throw ConflictException() }
accessRepository.delete(access) AccessRepository.delete(access)
val secret = OTP.generateSecret() val secret = OTP.generateSecret()
accountRepository.insert { AccountRepository.insert {
val salt = SecureHasher.generateSalt() val salt = SecureHasher.generateSalt()
this.username = request.username this.username = request.username
@@ -60,7 +56,7 @@ class AccountService(
if (principal.role != OWNER) if (principal.role != OWNER)
throw ForbiddenException() throw ForbiddenException()
return accountRepository return AccountRepository
.all() .all()
.map(AccountEntity::toResponse) .map(AccountEntity::toResponse)
} }
@@ -77,7 +73,7 @@ class AccountService(
if (principal.role == OWNER) if (principal.role == OWNER)
throw ForbiddenException() throw ForbiddenException()
accountRepository.delete(principal.account) AccountRepository.delete(principal.account)
} }
fun putCurrentUsername( fun putCurrentUsername(
@@ -88,10 +84,10 @@ class AccountService(
if (!OTP.verify(principal.account.secret, request.otp)) if (!OTP.verify(principal.account.secret, request.otp))
throw UnauthorizedException() throw UnauthorizedException()
accountRepository.update(principal.account) { AccountRepository.update(principal.account) {
this.username = request.username this.username = request.username
} }
sessionRepository.delete(principal.account.sessions()) SessionRepository.delete(principal.account.sessions())
sessions.clear<AuthenticationSession>() sessions.clear<AuthenticationSession>()
} }
@@ -104,13 +100,13 @@ class AccountService(
if (!OTP.verify(principal.account.secret, request.otp)) if (!OTP.verify(principal.account.secret, request.otp))
throw UnauthorizedException() throw UnauthorizedException()
accountRepository.update(principal.account) { AccountRepository.update(principal.account) {
val salt = SecureHasher.generateSalt() val salt = SecureHasher.generateSalt()
this.passwordHash = SecureHasher.hash(request.password, salt) this.passwordHash = SecureHasher.hash(request.password, salt)
this.passwordSalt = salt this.passwordSalt = salt
} }
sessionRepository.delete(principal.account.sessions()) SessionRepository.delete(principal.account.sessions())
sessions.clear<AuthenticationSession>() sessions.clear<AuthenticationSession>()
} }
@@ -124,10 +120,10 @@ class AccountService(
throw UnauthorizedException() throw UnauthorizedException()
val secret = OTP.generateSecret() val secret = OTP.generateSecret()
accountRepository.update(principal.account) { AccountRepository.update(principal.account) {
this.secret = secret this.secret = secret
} }
sessionRepository.delete(principal.account.sessions()) SessionRepository.delete(principal.account.sessions())
sessions.clear<AuthenticationSession>() sessions.clear<AuthenticationSession>()
@@ -148,14 +144,14 @@ class AccountService(
sessions: CurrentSession, sessions: CurrentSession,
id: Long, id: Long,
) { ) {
val session = sessionRepository val session = SessionRepository
.singleOrNull { SessionTable.id eq id } .singleOrNull { SessionTable.id eq id }
?: throw NotFoundException() ?: throw NotFoundException()
if (session.account().id != principal.account.id) if (session.account().id != principal.account.id)
throw ForbiddenException() throw ForbiddenException()
sessionRepository.delete(session) SessionRepository.delete(session)
if (session.id == principal.session.id) if (session.id == principal.session.id)
sessions.clear<AuthenticationSession>() sessions.clear<AuthenticationSession>()
@@ -165,14 +161,14 @@ class AccountService(
principal: AuthenticationPrincipal, principal: AuthenticationPrincipal,
id: Long, id: Long,
) { ) {
val account = accountRepository val account = AccountRepository
.singleOrNull { AccountTable.id eq id } .singleOrNull { AccountTable.id eq id }
?: throw NotFoundException() ?: throw NotFoundException()
if (account.role !in principal.role.children) if (account.role !in principal.role.children)
throw ForbiddenException() throw ForbiddenException()
accountRepository.delete(account) AccountRepository.delete(account)
} }
fun putIdRole( fun putIdRole(
@@ -180,7 +176,7 @@ class AccountService(
id: Long, id: Long,
request: PutAccountIdRoleRequest, request: PutAccountIdRoleRequest,
) { ) {
val account = accountRepository val account = AccountRepository
.singleOrNull { AccountTable.id eq id } .singleOrNull { AccountTable.id eq id }
?: throw NotFoundException() ?: throw NotFoundException()
@@ -190,7 +186,7 @@ class AccountService(
if (request.role !in principal.role.children) if (request.role !in principal.role.children)
throw ForbiddenException() throw ForbiddenException()
accountRepository.update(account) { AccountRepository.update(account) {
role = request.role role = request.role
} }
} }

View File

@@ -4,7 +4,7 @@ import com.infendro.account.dto.response.RoleResponse
import com.infendro.account.dto.response.toResponse import com.infendro.account.dto.response.toResponse
import com.infendro.account.model.Role import com.infendro.account.model.Role
class RoleService { object RoleService {
fun getAll(): List<RoleResponse> { fun getAll(): List<RoleResponse> {
return Role.ALL return Role.ALL
.map(Role::toResponse) .map(Role::toResponse)

View File

@@ -22,15 +22,12 @@ import io.ktor.server.sessions.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
class SessionService( object SessionService {
private val accountRepository: AccountRepository,
private val sessionRepository: SessionRepository,
) {
fun post( fun post(
sessions: CurrentSession, sessions: CurrentSession,
request: PostSessionRequest, request: PostSessionRequest,
) { ) {
val account = accountRepository val account = AccountRepository
.singleOrNull { AccountTable.username eq request.username } .singleOrNull { AccountTable.username eq request.username }
?: throw NotFoundException() ?: throw NotFoundException()
@@ -41,7 +38,7 @@ class SessionService(
throw UnauthorizedException() throw UnauthorizedException()
val token = TokenGenerator.generate() val token = TokenGenerator.generate()
sessionRepository.insert { SessionRepository.insert {
this.tokenHash = Hasher.hash(token) this.tokenHash = Hasher.hash(token)
this.account = account this.account = account
} }
@@ -57,7 +54,7 @@ class SessionService(
if (principal.role == USER) if (principal.role == USER)
throw ForbiddenException() throw ForbiddenException()
return accountRepository return AccountRepository
.all { AccountTable.role inList principal.role.children } .all { AccountTable.role inList principal.role.children }
.flatMap { it.sessions() } .flatMap { it.sessions() }
.map(SessionEntity::toResponse) .map(SessionEntity::toResponse)
@@ -70,14 +67,14 @@ class SessionService(
if (principal.role == USER) if (principal.role == USER)
throw ForbiddenException() throw ForbiddenException()
val session = sessionRepository val session = SessionRepository
.singleOrNull { SessionTable.id eq id } .singleOrNull { SessionTable.id eq id }
?: throw NotFoundException() ?: throw NotFoundException()
if (session.account().role !in principal.role.children) if (session.account().role !in principal.role.children)
throw ForbiddenException() throw ForbiddenException()
sessionRepository.delete(session) SessionRepository.delete(session)
} }
fun getCurrent( fun getCurrent(
@@ -90,7 +87,7 @@ class SessionService(
principal: AuthenticationPrincipal, principal: AuthenticationPrincipal,
sessions: CurrentSession, sessions: CurrentSession,
) { ) {
sessionRepository.delete(principal.session) SessionRepository.delete(principal.session)
sessions.clear<AuthenticationSession>() sessions.clear<AuthenticationSession>()
} }

View File

@@ -6,8 +6,8 @@ import com.infendro.kdf.PBKDF2
import com.infendro.random.PRNG import com.infendro.random.PRNG
object SecureHasher { object SecureHasher {
private val kdf = PBKDF2(SHA256)
private val random = PRNG.HMAC private val random = PRNG.HMAC
private val kdf = PBKDF2(SHA256)
fun hash( fun hash(
value: String, value: String,