Upgrade dependencies
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
import com.infendro.plugin.infendro
|
||||
|
||||
group = "com.infendro.account"
|
||||
version = "0.0.1"
|
||||
|
||||
@@ -13,7 +15,7 @@ plugins {
|
||||
}
|
||||
|
||||
repositories {
|
||||
maven("https://git.infendro.com/api/packages/Infendro/maven")
|
||||
infendro()
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
@@ -27,23 +29,25 @@ dependencies {
|
||||
implementation(libs.ktor.server.contentNegotiation)
|
||||
implementation(libs.ktor.server.serialization)
|
||||
implementation(libs.ktor.server.validation)
|
||||
|
||||
implementation(libs.exposed)
|
||||
implementation(libs.exposed.dao)
|
||||
implementation(libs.exposed.jdbc)
|
||||
implementation(libs.migration)
|
||||
implementation(libs.postgres)
|
||||
implementation(libs.prng)
|
||||
implementation(libs.otp)
|
||||
implementation(libs.kdf)
|
||||
implementation(libs.hash)
|
||||
implementation(libs.bytearray)
|
||||
|
||||
implementation(libs.bytes)
|
||||
implementation(libs.encoding)
|
||||
implementation(libs.hash)
|
||||
implementation(libs.kdf)
|
||||
implementation(libs.otp)
|
||||
implementation(libs.prng)
|
||||
|
||||
implementation(libs.logback)
|
||||
}
|
||||
|
||||
kotlin {
|
||||
compilerOptions {
|
||||
freeCompilerArgs.add("-opt-in=kotlin.ExperimentalUnsignedTypes")
|
||||
freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
[versions]
|
||||
infendro = "1.0.0"
|
||||
kotlin = "2.2.21"
|
||||
ktor = "3.3.3"
|
||||
exposed = "0.61.0"
|
||||
migration = "1.1.0"
|
||||
postgres = "42.7.8"
|
||||
prng = "1.2.0"
|
||||
otp = "1.2.0"
|
||||
kdf = "1.0.3"
|
||||
hash = "1.3.2"
|
||||
bytearray = "1.2.4"
|
||||
encoding = "1.2.0"
|
||||
logback = "1.5.22"
|
||||
infendro = "1.1.0"
|
||||
kotlin = "2.3.0"
|
||||
ktor = "3.4.0"
|
||||
|
||||
exposed = "1.0.0"
|
||||
migration = "1.2.0"
|
||||
postgres = "42.7.10"
|
||||
|
||||
bytes = "1.4.0"
|
||||
encoding = "1.3.0"
|
||||
hash = "1.6.0"
|
||||
kdf = "1.2.0"
|
||||
otp = "1.3.0"
|
||||
prng = "1.3.0"
|
||||
|
||||
logback = "1.5.32"
|
||||
|
||||
[plugins]
|
||||
infendro = { id = "com.infendro.plugin", version.ref = "infendro" }
|
||||
@@ -36,11 +39,11 @@ exposed-jdbc = { module = "org.jetbrains.exposed:exposed-jdbc", version.ref = "e
|
||||
migration = { module = "com.infendro:migration-ktor", version.ref = "migration" }
|
||||
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
|
||||
|
||||
prng = { module = "com.infendro:prng", version.ref = "prng" }
|
||||
otp = { module = "com.infendro:otp", version.ref = "otp" }
|
||||
kdf = { module = "com.infendro:kdf", version.ref = "kdf" }
|
||||
hash = { module = "com.infendro:hash", version.ref = "hash" }
|
||||
bytearray = { module = "com.infendro:bytearray", version.ref = "bytearray" }
|
||||
bytes = { module = "com.infendro:bytes", version.ref = "bytes" }
|
||||
encoding = { module = "com.infendro:encoding", version.ref = "encoding" }
|
||||
hash = { module = "com.infendro:hash", version.ref = "hash" }
|
||||
kdf = { module = "com.infendro:kdf", version.ref = "kdf" }
|
||||
otp = { module = "com.infendro:otp", version.ref = "otp" }
|
||||
prng = { module = "com.infendro:prng", version.ref = "prng" }
|
||||
|
||||
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||
|
||||
@@ -2,10 +2,10 @@ package com.infendro.account.config
|
||||
|
||||
import com.infendro.account.model.Role
|
||||
import com.infendro.account.model.repository.AccountRepository
|
||||
import com.infendro.account.util.SecureHasher
|
||||
import com.infendro.account.util.PasswordHasher
|
||||
import com.infendro.ktor.migration.Migration
|
||||
import io.ktor.server.application.*
|
||||
import org.jetbrains.exposed.sql.Database
|
||||
import org.jetbrains.exposed.v1.jdbc.Database
|
||||
|
||||
fun Application.configureDatabase() {
|
||||
val config = configuration
|
||||
@@ -55,14 +55,14 @@ fun Application.configureDatabase() {
|
||||
REFERENCES account (id) ON DELETE CASCADE
|
||||
NOT NULL
|
||||
);
|
||||
""".trimIndent()
|
||||
""".trimIndent(),
|
||||
)
|
||||
|
||||
AccountRepository.insert {
|
||||
val salt = SecureHasher.generateSalt()
|
||||
val salt = PasswordHasher.generateSalt()
|
||||
|
||||
this.username = "infendro"
|
||||
this.passwordHash = SecureHasher.hash("password", salt)
|
||||
this.passwordHash = PasswordHasher.hash("password", salt)
|
||||
this.passwordSalt = salt
|
||||
this.secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA===="
|
||||
this.role = Role.OWNER
|
||||
|
||||
@@ -11,7 +11,7 @@ import io.ktor.server.application.*
|
||||
import io.ktor.server.auth.*
|
||||
import io.ktor.server.sessions.*
|
||||
import kotlinx.serialization.Serializable
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
@Serializable
|
||||
|
||||
@@ -18,12 +18,10 @@ data class PostAccountRequest(
|
||||
)
|
||||
}
|
||||
|
||||
fun RequestValidationConfig.validatePostAccountRequest() {
|
||||
validate<PostAccountRequest> { request ->
|
||||
fun RequestValidationConfig.validatePostAccountRequest() = validate<PostAccountRequest> { request ->
|
||||
when {
|
||||
!USERNAME.matches(request.username) -> Invalid("")
|
||||
!PASSWORD.matches(request.password) -> Invalid("")
|
||||
else -> Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,9 @@ data class PutAccountCurrentPasswordRequest(
|
||||
val otp: String,
|
||||
)
|
||||
|
||||
fun RequestValidationConfig.validatePutAccountCurrentPasswordRequest() {
|
||||
validate<PutAccountCurrentPasswordRequest> { request ->
|
||||
fun RequestValidationConfig.validatePutAccountCurrentPasswordRequest() = validate<PutAccountCurrentPasswordRequest> { request ->
|
||||
when {
|
||||
!PASSWORD.matches(request.password) -> Invalid("")
|
||||
else -> Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,11 +11,9 @@ data class PutAccountCurrentUsernameRequest(
|
||||
val otp: String,
|
||||
)
|
||||
|
||||
fun RequestValidationConfig.validatePutAccountCurrentUsernameRequest() {
|
||||
validate<PutAccountCurrentUsernameRequest> { request ->
|
||||
fun RequestValidationConfig.validatePutAccountCurrentUsernameRequest() = validate<PutAccountCurrentUsernameRequest> { request ->
|
||||
when {
|
||||
!USERNAME.matches(request.username) -> Invalid("")
|
||||
else -> Valid
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,9 @@ data class AccountResponse(
|
||||
val sessions: List<SessionResponse>,
|
||||
)
|
||||
|
||||
fun AccountEntity.toResponse(): AccountResponse {
|
||||
return AccountResponse(
|
||||
fun AccountEntity.toResponse() = AccountResponse(
|
||||
id = id.value,
|
||||
username = username,
|
||||
role = role,
|
||||
sessions = sessions().map(SessionEntity::toResponse),
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -9,9 +9,7 @@ data class SessionResponse(
|
||||
val accountId: Long,
|
||||
)
|
||||
|
||||
fun SessionEntity.toResponse(): SessionResponse {
|
||||
return SessionResponse(
|
||||
fun SessionEntity.toResponse() = SessionResponse(
|
||||
id = id.value,
|
||||
accountId = account().id.value,
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -13,16 +13,5 @@ enum class Role(
|
||||
}
|
||||
}
|
||||
|
||||
infix fun List<Role>.except(
|
||||
roles: List<Role>,
|
||||
): List<Role> {
|
||||
return filterNot { it in roles }
|
||||
.toList()
|
||||
}
|
||||
|
||||
infix fun List<Role>.except(
|
||||
role: Role,
|
||||
): List<Role> {
|
||||
return filterNot { it == role }
|
||||
.toList()
|
||||
}
|
||||
infix fun List<Role>.except(roles: List<Role>) = filterNot { it in roles }
|
||||
infix fun List<Role>.except(role: Role) = filterNot { it == role }
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
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
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.LongIdTable
|
||||
import org.jetbrains.exposed.v1.dao.LongEntity
|
||||
import org.jetbrains.exposed.v1.dao.LongEntityClass
|
||||
|
||||
object AccessTable : LongIdTable("access", "id") {
|
||||
val tokenHash = text("token_hash")
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
package com.infendro.account.model.entity
|
||||
|
||||
import com.infendro.account.model.Role
|
||||
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
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.LongIdTable
|
||||
import org.jetbrains.exposed.v1.dao.LongEntity
|
||||
import org.jetbrains.exposed.v1.dao.LongEntityClass
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
|
||||
object AccountTable : LongIdTable("account", "id") {
|
||||
val username = text("username")
|
||||
@@ -20,7 +20,7 @@ object AccountTable : LongIdTable("account", "id") {
|
||||
else -> throw Error()
|
||||
}
|
||||
},
|
||||
toDb = { it.name }
|
||||
toDb = { it.name },
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
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
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||
import org.jetbrains.exposed.v1.core.dao.id.LongIdTable
|
||||
import org.jetbrains.exposed.v1.dao.LongEntity
|
||||
import org.jetbrains.exposed.v1.dao.LongEntityClass
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
|
||||
object SessionTable : LongIdTable("session", "id") {
|
||||
val tokenHash = text("token_hash")
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
package com.infendro.account.model.repository
|
||||
|
||||
import org.jetbrains.exposed.dao.LongEntity
|
||||
import org.jetbrains.exposed.dao.LongEntityClass
|
||||
import org.jetbrains.exposed.sql.Op
|
||||
import org.jetbrains.exposed.sql.transactions.transaction
|
||||
import org.jetbrains.exposed.v1.core.Op
|
||||
import org.jetbrains.exposed.v1.dao.LongEntity
|
||||
import org.jetbrains.exposed.v1.dao.LongEntityClass
|
||||
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||
|
||||
abstract class Repository<ENTITY : LongEntity>(
|
||||
private val entityClass: LongEntityClass<ENTITY>,
|
||||
@@ -12,113 +12,75 @@ abstract class Repository<ENTITY : LongEntity>(
|
||||
entityClass.all().toList()
|
||||
}
|
||||
|
||||
fun all(
|
||||
where: Op<Boolean>,
|
||||
): List<ENTITY> = transaction {
|
||||
fun all(where: Op<Boolean>): List<ENTITY> = transaction {
|
||||
entityClass.find(where).toList()
|
||||
}
|
||||
|
||||
fun all(
|
||||
where: () -> Op<Boolean>,
|
||||
): List<ENTITY> {
|
||||
fun all(where: () -> Op<Boolean>): List<ENTITY> {
|
||||
return all(where())
|
||||
}
|
||||
|
||||
fun single(
|
||||
where: Op<Boolean>,
|
||||
): ENTITY = transaction {
|
||||
fun single(where: Op<Boolean>): ENTITY = transaction {
|
||||
entityClass.find(where).single()
|
||||
}
|
||||
|
||||
fun single(
|
||||
where: () -> Op<Boolean>,
|
||||
): ENTITY {
|
||||
fun single(where: () -> Op<Boolean>): ENTITY {
|
||||
return single(where())
|
||||
}
|
||||
|
||||
fun singleOrNull(
|
||||
where: Op<Boolean>,
|
||||
): ENTITY? = transaction {
|
||||
fun singleOrNull(where: Op<Boolean>): ENTITY? = transaction {
|
||||
entityClass.find(where).singleOrNull()
|
||||
}
|
||||
|
||||
fun singleOrNull(
|
||||
where: () -> Op<Boolean>,
|
||||
): ENTITY? {
|
||||
fun singleOrNull(where: () -> Op<Boolean>): ENTITY? {
|
||||
return singleOrNull(where())
|
||||
}
|
||||
|
||||
fun exists(
|
||||
where: Op<Boolean>,
|
||||
): Boolean = transaction {
|
||||
fun exists(where: Op<Boolean>): Boolean = transaction {
|
||||
entityClass.find(where).any()
|
||||
}
|
||||
|
||||
fun exists(
|
||||
where: () -> Op<Boolean>,
|
||||
): Boolean {
|
||||
fun exists(where: () -> Op<Boolean>): Boolean {
|
||||
return exists(where())
|
||||
}
|
||||
|
||||
fun insert(
|
||||
block: ENTITY.() -> Unit,
|
||||
): Unit = transaction {
|
||||
fun insert(block: ENTITY.() -> Unit): Unit = transaction {
|
||||
entityClass.new(block)
|
||||
}
|
||||
|
||||
fun update(
|
||||
entity: ENTITY,
|
||||
block: ENTITY.() -> Unit,
|
||||
): Unit = transaction {
|
||||
fun update(entity: ENTITY, block: ENTITY.() -> Unit): Unit = transaction {
|
||||
entity.block()
|
||||
}
|
||||
|
||||
fun update(
|
||||
entities: Iterable<ENTITY>,
|
||||
block: ENTITY.() -> Unit,
|
||||
) {
|
||||
fun update(entities: Iterable<ENTITY>, block: ENTITY.() -> Unit) {
|
||||
for (entity in entities) {
|
||||
update(entity, block)
|
||||
}
|
||||
}
|
||||
|
||||
fun update(
|
||||
where: Op<Boolean>,
|
||||
block: ENTITY.() -> Unit,
|
||||
) {
|
||||
fun update(where: Op<Boolean>, block: ENTITY.() -> Unit) {
|
||||
update(all(where), block)
|
||||
}
|
||||
|
||||
fun update(
|
||||
where: () -> Op<Boolean>,
|
||||
block: ENTITY.() -> Unit,
|
||||
) {
|
||||
fun update(where: () -> Op<Boolean>, block: ENTITY.() -> Unit) {
|
||||
update(where(), block)
|
||||
}
|
||||
|
||||
fun delete(
|
||||
entity: ENTITY,
|
||||
): Unit = transaction {
|
||||
fun delete(entity: ENTITY): Unit = transaction {
|
||||
entity.delete()
|
||||
}
|
||||
|
||||
fun delete(
|
||||
entities: Iterable<ENTITY>,
|
||||
) {
|
||||
fun delete(entities: Iterable<ENTITY>) {
|
||||
for (entity in entities) {
|
||||
delete(entity)
|
||||
}
|
||||
}
|
||||
|
||||
fun delete(
|
||||
where: Op<Boolean>,
|
||||
) {
|
||||
fun delete(where: Op<Boolean>) {
|
||||
delete(all(where))
|
||||
}
|
||||
|
||||
fun delete(
|
||||
where: () -> Op<Boolean>,
|
||||
) {
|
||||
fun delete(where: () -> Op<Boolean>) {
|
||||
delete(where())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,10 @@ import com.infendro.account.model.entity.AccessTable
|
||||
import com.infendro.account.model.repository.AccessRepository
|
||||
import com.infendro.account.util.Hasher
|
||||
import com.infendro.account.util.TokenGenerator
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
|
||||
object AccessService {
|
||||
fun post(
|
||||
principal: AuthenticationPrincipal,
|
||||
): PostAccessResponse {
|
||||
fun post(principal: AuthenticationPrincipal): PostAccessResponse {
|
||||
if (principal.role != OWNER)
|
||||
throw ForbiddenException()
|
||||
|
||||
@@ -26,13 +24,11 @@ object AccessService {
|
||||
}
|
||||
|
||||
return PostAccessResponse(
|
||||
token = token
|
||||
token = token,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAll(
|
||||
principal: AuthenticationPrincipal,
|
||||
): List<AccessResponse> {
|
||||
fun getAll(principal: AuthenticationPrincipal): List<AccessResponse> {
|
||||
if (principal.role != OWNER)
|
||||
throw ForbiddenException()
|
||||
|
||||
@@ -41,10 +37,7 @@ object AccessService {
|
||||
.map { it.toResponse() }
|
||||
}
|
||||
|
||||
fun deleteId(
|
||||
principal: AuthenticationPrincipal,
|
||||
id: Long,
|
||||
) {
|
||||
fun deleteId(principal: AuthenticationPrincipal, id: Long) {
|
||||
if (principal.role != OWNER)
|
||||
throw ForbiddenException()
|
||||
|
||||
|
||||
@@ -16,14 +16,12 @@ import com.infendro.account.model.repository.AccountRepository
|
||||
import com.infendro.account.model.repository.SessionRepository
|
||||
import com.infendro.account.util.Hasher
|
||||
import com.infendro.account.util.OTP
|
||||
import com.infendro.account.util.SecureHasher
|
||||
import com.infendro.account.util.PasswordHasher
|
||||
import io.ktor.server.sessions.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
|
||||
object AccountService {
|
||||
fun post(
|
||||
request: PostAccountRequest,
|
||||
): PostAccountResponse {
|
||||
fun post(request: PostAccountRequest): PostAccountResponse {
|
||||
val access = AccessRepository
|
||||
.singleOrNull { AccessTable.tokenHash eq Hasher.hash(request.access.token) }
|
||||
?: throw UnauthorizedException()
|
||||
@@ -36,23 +34,21 @@ object AccountService {
|
||||
|
||||
val secret = OTP.generateSecret()
|
||||
AccountRepository.insert {
|
||||
val salt = SecureHasher.generateSalt()
|
||||
val salt = PasswordHasher.generateSalt()
|
||||
|
||||
this.username = request.username
|
||||
this.passwordHash = SecureHasher.hash(request.password, salt)
|
||||
this.passwordHash = PasswordHasher.hash(request.password, salt)
|
||||
this.passwordSalt = salt
|
||||
this.secret = secret
|
||||
this.role = USER
|
||||
}
|
||||
|
||||
return PostAccountResponse(
|
||||
secret = secret
|
||||
secret = secret,
|
||||
)
|
||||
}
|
||||
|
||||
fun getAll(
|
||||
principal: AuthenticationPrincipal,
|
||||
): List<AccountResponse> {
|
||||
fun getAll(principal: AuthenticationPrincipal): List<AccountResponse> {
|
||||
if (principal.role != OWNER)
|
||||
throw ForbiddenException()
|
||||
|
||||
@@ -61,26 +57,18 @@ object AccountService {
|
||||
.map(AccountEntity::toResponse)
|
||||
}
|
||||
|
||||
fun getCurrent(
|
||||
principal: AuthenticationPrincipal,
|
||||
): AccountResponse {
|
||||
fun getCurrent(principal: AuthenticationPrincipal): AccountResponse {
|
||||
return principal.account.toResponse()
|
||||
}
|
||||
|
||||
fun deleteCurrent(
|
||||
principal: AuthenticationPrincipal,
|
||||
) {
|
||||
fun deleteCurrent(principal: AuthenticationPrincipal) {
|
||||
if (principal.role == OWNER)
|
||||
throw ForbiddenException()
|
||||
|
||||
AccountRepository.delete(principal.account)
|
||||
}
|
||||
|
||||
fun putCurrentUsername(
|
||||
principal: AuthenticationPrincipal,
|
||||
sessions: CurrentSession,
|
||||
request: PutAccountCurrentUsernameRequest,
|
||||
) {
|
||||
fun putCurrentUsername(principal: AuthenticationPrincipal, sessions: CurrentSession, request: PutAccountCurrentUsernameRequest) {
|
||||
if (!OTP.verify(principal.account.secret, request.otp))
|
||||
throw UnauthorizedException()
|
||||
|
||||
@@ -92,18 +80,14 @@ object AccountService {
|
||||
sessions.clear<AuthenticationSession>()
|
||||
}
|
||||
|
||||
fun putCurrentPassword(
|
||||
principal: AuthenticationPrincipal,
|
||||
sessions: CurrentSession,
|
||||
request: PutAccountCurrentPasswordRequest,
|
||||
) {
|
||||
fun putCurrentPassword(principal: AuthenticationPrincipal, sessions: CurrentSession, request: PutAccountCurrentPasswordRequest) {
|
||||
if (!OTP.verify(principal.account.secret, request.otp))
|
||||
throw UnauthorizedException()
|
||||
|
||||
AccountRepository.update(principal.account) {
|
||||
val salt = SecureHasher.generateSalt()
|
||||
val salt = PasswordHasher.generateSalt()
|
||||
|
||||
this.passwordHash = SecureHasher.hash(request.password, salt)
|
||||
this.passwordHash = PasswordHasher.hash(request.password, salt)
|
||||
this.passwordSalt = salt
|
||||
}
|
||||
SessionRepository.delete(principal.account.sessions())
|
||||
@@ -111,12 +95,8 @@ object AccountService {
|
||||
sessions.clear<AuthenticationSession>()
|
||||
}
|
||||
|
||||
fun postCurrentSecret(
|
||||
principal: AuthenticationPrincipal,
|
||||
sessions: CurrentSession,
|
||||
request: PostAccountCurrentSecretRequest,
|
||||
): PostAccountCurrentSecretResponse {
|
||||
if (principal.account.passwordHash != SecureHasher.hash(request.password, principal.account.passwordSalt))
|
||||
fun postCurrentSecret(principal: AuthenticationPrincipal, sessions: CurrentSession, request: PostAccountCurrentSecretRequest): PostAccountCurrentSecretResponse {
|
||||
if (principal.account.passwordHash != PasswordHasher.hash(request.password, principal.account.passwordSalt))
|
||||
throw UnauthorizedException()
|
||||
|
||||
val secret = OTP.generateSecret()
|
||||
@@ -128,22 +108,16 @@ object AccountService {
|
||||
sessions.clear<AuthenticationSession>()
|
||||
|
||||
return PostAccountCurrentSecretResponse(
|
||||
secret = secret
|
||||
secret = secret,
|
||||
)
|
||||
}
|
||||
|
||||
fun getCurrentSessionAll(
|
||||
principal: AuthenticationPrincipal,
|
||||
): List<SessionResponse> {
|
||||
fun getCurrentSessionAll(principal: AuthenticationPrincipal): List<SessionResponse> {
|
||||
return principal.account.sessions()
|
||||
.map(SessionEntity::toResponse)
|
||||
}
|
||||
|
||||
fun deleteCurrentSessionId(
|
||||
principal: AuthenticationPrincipal,
|
||||
sessions: CurrentSession,
|
||||
id: Long,
|
||||
) {
|
||||
fun deleteCurrentSessionId(principal: AuthenticationPrincipal, sessions: CurrentSession, id: Long) {
|
||||
val session = SessionRepository
|
||||
.singleOrNull { SessionTable.id eq id }
|
||||
?: throw NotFoundException()
|
||||
@@ -157,10 +131,7 @@ object AccountService {
|
||||
sessions.clear<AuthenticationSession>()
|
||||
}
|
||||
|
||||
fun deleteId(
|
||||
principal: AuthenticationPrincipal,
|
||||
id: Long,
|
||||
) {
|
||||
fun deleteId(principal: AuthenticationPrincipal, id: Long) {
|
||||
val account = AccountRepository
|
||||
.singleOrNull { AccountTable.id eq id }
|
||||
?: throw NotFoundException()
|
||||
@@ -171,11 +142,7 @@ object AccountService {
|
||||
AccountRepository.delete(account)
|
||||
}
|
||||
|
||||
fun putIdRole(
|
||||
principal: AuthenticationPrincipal,
|
||||
id: Long,
|
||||
request: PutAccountIdRoleRequest,
|
||||
) {
|
||||
fun putIdRole(principal: AuthenticationPrincipal, id: Long, request: PutAccountIdRoleRequest) {
|
||||
val account = AccountRepository
|
||||
.singleOrNull { AccountTable.id eq id }
|
||||
?: throw NotFoundException()
|
||||
|
||||
@@ -16,17 +16,14 @@ import com.infendro.account.model.repository.AccountRepository
|
||||
import com.infendro.account.model.repository.SessionRepository
|
||||
import com.infendro.account.util.Hasher
|
||||
import com.infendro.account.util.OTP
|
||||
import com.infendro.account.util.SecureHasher
|
||||
import com.infendro.account.util.PasswordHasher
|
||||
import com.infendro.account.util.TokenGenerator
|
||||
import io.ktor.server.sessions.*
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
||||
import org.jetbrains.exposed.v1.core.eq
|
||||
import org.jetbrains.exposed.v1.core.inList
|
||||
|
||||
object SessionService {
|
||||
fun post(
|
||||
sessions: CurrentSession,
|
||||
request: PostSessionRequest,
|
||||
) {
|
||||
fun post(sessions: CurrentSession, request: PostSessionRequest) {
|
||||
val account = AccountRepository
|
||||
.singleOrNull { AccountTable.username eq request.username }
|
||||
?: throw NotFoundException()
|
||||
@@ -34,7 +31,7 @@ object SessionService {
|
||||
if (!OTP.verify(account.secret, request.otp))
|
||||
throw UnauthorizedException()
|
||||
|
||||
if (account.passwordHash != SecureHasher.hash(request.password, account.passwordSalt))
|
||||
if (account.passwordHash != PasswordHasher.hash(request.password, account.passwordSalt))
|
||||
throw UnauthorizedException()
|
||||
|
||||
val token = TokenGenerator.generate()
|
||||
@@ -43,14 +40,10 @@ object SessionService {
|
||||
this.account = account
|
||||
}
|
||||
|
||||
sessions.set(
|
||||
AuthenticationSession(token)
|
||||
)
|
||||
sessions.set(AuthenticationSession(token))
|
||||
}
|
||||
|
||||
fun getAll(
|
||||
principal: AuthenticationPrincipal,
|
||||
): List<SessionResponse> {
|
||||
fun getAll(principal: AuthenticationPrincipal): List<SessionResponse> {
|
||||
if (principal.role == USER)
|
||||
throw ForbiddenException()
|
||||
|
||||
@@ -60,10 +53,7 @@ object SessionService {
|
||||
.map(SessionEntity::toResponse)
|
||||
}
|
||||
|
||||
fun deleteId(
|
||||
principal: AuthenticationPrincipal,
|
||||
id: Long,
|
||||
) {
|
||||
fun deleteId(principal: AuthenticationPrincipal, id: Long) {
|
||||
if (principal.role == USER)
|
||||
throw ForbiddenException()
|
||||
|
||||
@@ -77,16 +67,11 @@ object SessionService {
|
||||
SessionRepository.delete(session)
|
||||
}
|
||||
|
||||
fun getCurrent(
|
||||
principal: AuthenticationPrincipal,
|
||||
): SessionResponse {
|
||||
fun getCurrent(principal: AuthenticationPrincipal): SessionResponse {
|
||||
return principal.session.toResponse()
|
||||
}
|
||||
|
||||
fun deleteCurrent(
|
||||
principal: AuthenticationPrincipal,
|
||||
sessions: CurrentSession,
|
||||
) {
|
||||
fun deleteCurrent(principal: AuthenticationPrincipal, sessions: CurrentSession) {
|
||||
SessionRepository.delete(principal.session)
|
||||
|
||||
sessions.clear<AuthenticationSession>()
|
||||
|
||||
@@ -1,15 +1,11 @@
|
||||
package com.infendro.account.util
|
||||
|
||||
import com.infendro.bytearray.decodeToString
|
||||
import com.infendro.bytearray.encodeToUByteArray
|
||||
import com.infendro.encoding.Base16
|
||||
import com.infendro.hash.sha2.`SHA-256`
|
||||
|
||||
object Hasher {
|
||||
fun hash(
|
||||
value: String,
|
||||
): String {
|
||||
val bytes = `SHA-256`.hash(value.encodeToUByteArray())
|
||||
fun hash(value: String): String {
|
||||
val bytes = `SHA-256`.hash(value.encodeToByteArray())
|
||||
return Base16.encode(bytes).decodeToString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,27 @@
|
||||
package com.infendro.account.util
|
||||
|
||||
import com.infendro.bytearray.decodeToString
|
||||
import com.infendro.bytearray.encodeToUByteArray
|
||||
import com.infendro.encoding.Base32
|
||||
import com.infendro.hash.sha2.`SHA-256`
|
||||
import com.infendro.otp.SecretGenerator
|
||||
import com.infendro.otp.TOTP
|
||||
import com.infendro.random.csprng.`HMAC-DRBG`
|
||||
import kotlin.time.Clock
|
||||
import kotlin.time.Duration.Companion.seconds
|
||||
|
||||
object OTP {
|
||||
private val generator = SecretGenerator(`SHA-256`)
|
||||
private val random = `HMAC-DRBG`(`SHA-256`)
|
||||
private val totp = TOTP(
|
||||
function = `SHA-256`,
|
||||
length = 8,
|
||||
period = 30.seconds,
|
||||
)
|
||||
|
||||
fun verify(
|
||||
secret: String,
|
||||
otp: String,
|
||||
): Boolean {
|
||||
val bytes = Base32.decode(secret.encodeToUByteArray())
|
||||
fun verify(secret: String, otp: String): Boolean {
|
||||
val bytes = Base32.decode(secret.encodeToByteArray())
|
||||
return totp.verify(bytes, Clock.System.now(), otp)
|
||||
}
|
||||
|
||||
fun generateSecret(): String {
|
||||
val bytes = generator.generate()
|
||||
val bytes = random.nextBytes(32)
|
||||
return Base32.encode(bytes).decodeToString()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.infendro.account.util
|
||||
|
||||
import com.infendro.encoding.Base16
|
||||
import com.infendro.hash.sha2.`SHA-256`
|
||||
import com.infendro.kdf.PBKDF2
|
||||
import com.infendro.random.csprng.`HMAC-DRBG`
|
||||
|
||||
object PasswordHasher {
|
||||
private val random = `HMAC-DRBG`(`SHA-256`)
|
||||
private val kdf = PBKDF2(32, 100_000, `SHA-256`)
|
||||
|
||||
fun hash(value: String, salt: String): String {
|
||||
val value = value.encodeToByteArray()
|
||||
val salt = Base16.decode(salt.encodeToByteArray())
|
||||
return kdf.hash(value, salt)
|
||||
.let { Base16.encode(it).decodeToString() }
|
||||
}
|
||||
|
||||
fun generateSalt(): String {
|
||||
return random.nextBytes(16)
|
||||
.let { Base16.encode(it).decodeToString() }
|
||||
}
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
package com.infendro.account.util
|
||||
|
||||
import com.infendro.bytearray.decodeToString
|
||||
import com.infendro.bytearray.encodeToUByteArray
|
||||
import com.infendro.encoding.Base16
|
||||
import com.infendro.hash.sha2.`SHA-256`
|
||||
import com.infendro.kdf.PBKDF2
|
||||
import com.infendro.random.csprng.CSPRNG
|
||||
|
||||
object SecureHasher {
|
||||
private val random = CSPRNG.HMAC(`SHA-256`)
|
||||
private val kdf = PBKDF2(`SHA-256`)
|
||||
|
||||
fun hash(
|
||||
value: String,
|
||||
salt: String,
|
||||
): String {
|
||||
val value = value.encodeToByteArray()
|
||||
val salt = Base16.decode(salt.encodeToUByteArray())
|
||||
return kdf.hash(value, salt, 100_000, 32)
|
||||
.let { Base16.encode(it).decodeToString() }
|
||||
}
|
||||
|
||||
fun generateSalt(): String {
|
||||
return random.nextBytes(16)
|
||||
.let { Base16.encode(it).decodeToString() }
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,11 @@
|
||||
package com.infendro.account.util
|
||||
|
||||
import com.infendro.bytearray.decodeToString
|
||||
import com.infendro.encoding.Base16
|
||||
import com.infendro.hash.sha2.`SHA-256`
|
||||
import com.infendro.random.csprng.CSPRNG
|
||||
import com.infendro.random.csprng.`HMAC-DRBG`
|
||||
|
||||
object TokenGenerator {
|
||||
private val random = CSPRNG.HMAC(`SHA-256`)
|
||||
private val random = `HMAC-DRBG`(`SHA-256`)
|
||||
|
||||
fun generate(): String {
|
||||
return random.nextBytes(32)
|
||||
|
||||
Reference in New Issue
Block a user