Compare commits
4 Commits
53954ea4d9
...
develop
| Author | SHA1 | Date | |
|---|---|---|---|
|
9841b3bda3
|
|||
|
f5ca766293
|
|||
|
c718bc2c6a
|
|||
|
6d04f15c45
|
@@ -1,3 +1,5 @@
|
|||||||
|
import com.infendro.plugin.infendro
|
||||||
|
|
||||||
group = "com.infendro.account"
|
group = "com.infendro.account"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
|
|
||||||
@@ -5,17 +7,18 @@ application {
|
|||||||
mainClass.set("com.infendro.account.ApplicationKt")
|
mainClass.set("com.infendro.account.ApplicationKt")
|
||||||
}
|
}
|
||||||
|
|
||||||
repositories {
|
|
||||||
maven("https://git.infendro.com/api/packages/Infendro/maven")
|
|
||||||
mavenCentral()
|
|
||||||
}
|
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
|
alias(libs.plugins.infendro)
|
||||||
alias(libs.plugins.kotlin)
|
alias(libs.plugins.kotlin)
|
||||||
alias(libs.plugins.ktor)
|
alias(libs.plugins.ktor)
|
||||||
alias(libs.plugins.serialization)
|
alias(libs.plugins.serialization)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
infendro()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation(libs.ktor.server)
|
implementation(libs.ktor.server)
|
||||||
implementation(libs.ktor.server.cio)
|
implementation(libs.ktor.server.cio)
|
||||||
@@ -26,16 +29,20 @@ dependencies {
|
|||||||
implementation(libs.ktor.server.contentNegotiation)
|
implementation(libs.ktor.server.contentNegotiation)
|
||||||
implementation(libs.ktor.server.serialization)
|
implementation(libs.ktor.server.serialization)
|
||||||
implementation(libs.ktor.server.validation)
|
implementation(libs.ktor.server.validation)
|
||||||
|
|
||||||
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.migration)
|
||||||
implementation(libs.postgres)
|
implementation(libs.postgres)
|
||||||
implementation(libs.random)
|
|
||||||
implementation(libs.otp)
|
implementation(libs.bytes)
|
||||||
implementation(libs.kdf)
|
|
||||||
implementation(libs.hash)
|
|
||||||
implementation(libs.encoding)
|
implementation(libs.encoding)
|
||||||
|
implementation(libs.hash)
|
||||||
|
implementation(libs.kdf)
|
||||||
|
implementation(libs.otp)
|
||||||
|
implementation(libs.prng)
|
||||||
|
|
||||||
implementation(libs.logback)
|
implementation(libs.logback)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,23 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.2.10"
|
infendro = "1.1.0"
|
||||||
ktor = "3.2.3"
|
kotlin = "2.3.0"
|
||||||
exposed = "0.61.0"
|
ktor = "3.4.0"
|
||||||
migration = "1.1.0"
|
|
||||||
postgres = "42.7.5"
|
exposed = "1.0.0"
|
||||||
random = "1.0.0"
|
migration = "1.2.0"
|
||||||
otp = "1.1.1"
|
postgres = "42.7.10"
|
||||||
kdf = "1.0.3"
|
|
||||||
hash = "1.2.2"
|
bytes = "1.4.0"
|
||||||
encoding = "1.1.0"
|
encoding = "1.3.0"
|
||||||
logback = "1.5.18"
|
hash = "1.6.0"
|
||||||
|
kdf = "1.2.0"
|
||||||
|
otp = "1.3.0"
|
||||||
|
prng = "1.3.0"
|
||||||
|
|
||||||
|
logback = "1.5.32"
|
||||||
|
|
||||||
[plugins]
|
[plugins]
|
||||||
|
infendro = { id = "com.infendro.plugin", version.ref = "infendro" }
|
||||||
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
kotlin = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
|
||||||
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
ktor = { id = "io.ktor.plugin", version.ref = "ktor" }
|
||||||
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
|
||||||
@@ -30,13 +36,14 @@ 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" }
|
migration = { module = "com.infendro:migration-ktor", version.ref = "migration" }
|
||||||
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
|
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
|
||||||
|
|
||||||
random = { module = "com.infendro:random", version.ref = "random" }
|
bytes = { module = "com.infendro:bytes", version.ref = "bytes" }
|
||||||
otp = { module = "com.infendro:otp", version.ref = "otp" }
|
|
||||||
kdf = { module = "com.infendro:kdf", version.ref = "kdf" }
|
|
||||||
hash = { module = "com.infendro:hash", version.ref = "hash" }
|
|
||||||
encoding = { module = "com.infendro:encoding", version.ref = "encoding" }
|
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" }
|
logback = { module = "ch.qos.logback:logback-classic", version.ref = "logback" }
|
||||||
|
|||||||
@@ -1 +1,7 @@
|
|||||||
rootProject.name = "backend"
|
rootProject.name = "backend"
|
||||||
|
|
||||||
|
pluginManagement.repositories {
|
||||||
|
maven("https://git.infendro.com/api/packages/Infendro/maven")
|
||||||
|
gradlePluginPortal()
|
||||||
|
mavenCentral()
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,12 +1,11 @@
|
|||||||
package com.infendro.account
|
package com.infendro.account
|
||||||
|
|
||||||
import com.infendro.account.config.*
|
import com.infendro.account.config.*
|
||||||
|
import com.infendro.account.config.configureSession
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.cio.*
|
import io.ktor.server.cio.*
|
||||||
|
|
||||||
fun main(
|
fun main(args: Array<String>) = EngineMain.main(args)
|
||||||
args: Array<String>,
|
|
||||||
) = EngineMain.main(args)
|
|
||||||
|
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
configureDatabase()
|
configureDatabase()
|
||||||
@@ -15,6 +14,7 @@ fun Application.module() {
|
|||||||
configureValidation()
|
configureValidation()
|
||||||
configureException()
|
configureException()
|
||||||
|
|
||||||
|
configureSession()
|
||||||
configureSecurity()
|
configureSecurity()
|
||||||
configureRouting()
|
configureRouting()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
package com.infendro.account.config
|
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.ktor.migration.Migration
|
import com.infendro.ktor.migration.Migration
|
||||||
|
import initial
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.v1.jdbc.Database
|
||||||
|
|
||||||
fun Application.configureDatabase() {
|
fun Application.configureDatabase() {
|
||||||
val config = configuration
|
val config = configuration
|
||||||
@@ -17,56 +15,6 @@ fun Application.configureDatabase() {
|
|||||||
)
|
)
|
||||||
|
|
||||||
install(Migration) {
|
install(Migration) {
|
||||||
migration("initial") {
|
initial()
|
||||||
exec(
|
|
||||||
"""
|
|
||||||
CREATE TABLE access
|
|
||||||
(
|
|
||||||
id BIGSERIAL
|
|
||||||
PRIMARY KEY,
|
|
||||||
token_hash TEXT
|
|
||||||
NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE account
|
|
||||||
(
|
|
||||||
id BIGSERIAL
|
|
||||||
PRIMARY KEY,
|
|
||||||
username TEXT
|
|
||||||
UNIQUE
|
|
||||||
NOT NULL,
|
|
||||||
password_hash TEXT
|
|
||||||
NOT NULL,
|
|
||||||
password_salt TEXT
|
|
||||||
NOT NULL,
|
|
||||||
secret TEXT
|
|
||||||
NOT NULL,
|
|
||||||
role TEXT
|
|
||||||
NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE TABLE session
|
|
||||||
(
|
|
||||||
id BIGSERIAL
|
|
||||||
PRIMARY KEY,
|
|
||||||
token_hash TEXT
|
|
||||||
NOT NULL,
|
|
||||||
account_id BIGINT
|
|
||||||
REFERENCES account (id) ON DELETE CASCADE
|
|
||||||
NOT NULL
|
|
||||||
);
|
|
||||||
""".trimIndent()
|
|
||||||
)
|
|
||||||
|
|
||||||
AccountRepository.insert {
|
|
||||||
val salt = SecureHasher.generateSalt()
|
|
||||||
|
|
||||||
this.username = "infendro"
|
|
||||||
this.passwordHash = SecureHasher.hash("password", salt)
|
|
||||||
this.passwordSalt = salt
|
|
||||||
this.secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA===="
|
|
||||||
this.role = Role.OWNER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,25 +1,14 @@
|
|||||||
package com.infendro.account.config
|
package com.infendro.account.config
|
||||||
|
|
||||||
import com.infendro.account.exception.client.UnauthorizedException
|
|
||||||
import com.infendro.account.model.Role
|
import com.infendro.account.model.Role
|
||||||
import com.infendro.account.model.entity.AccountEntity
|
import com.infendro.account.model.entity.AccountEntity
|
||||||
import com.infendro.account.model.entity.SessionEntity
|
import com.infendro.account.model.entity.SessionEntity
|
||||||
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 io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.auth.*
|
import io.ktor.server.auth.*
|
||||||
import io.ktor.server.sessions.*
|
import io.ktor.server.sessions.*
|
||||||
import kotlinx.serialization.Serializable
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
|
||||||
import kotlin.time.Duration.Companion.days
|
|
||||||
|
|
||||||
@Serializable
|
data class Principal(
|
||||||
data class AuthenticationSession(
|
|
||||||
val token: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
data class AuthenticationPrincipal(
|
|
||||||
val account: AccountEntity,
|
val account: AccountEntity,
|
||||||
val session: SessionEntity,
|
val session: SessionEntity,
|
||||||
) {
|
) {
|
||||||
@@ -28,34 +17,20 @@ data class AuthenticationPrincipal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun Application.configureSecurity() {
|
fun Application.configureSecurity() {
|
||||||
val config = configuration
|
|
||||||
|
|
||||||
install(Sessions) {
|
|
||||||
cookie<AuthenticationSession>("Authentication") {
|
|
||||||
this.cookie.domain = config.security.cookie.domain
|
|
||||||
this.cookie.path = config.security.cookie.path
|
|
||||||
this.cookie.httpOnly = true
|
|
||||||
this.cookie.secure = config.security.cookie.secure
|
|
||||||
this.cookie.extensions["SameSite"] = "Strict"
|
|
||||||
this.cookie.maxAge = 7.days
|
|
||||||
}
|
|
||||||
}
|
|
||||||
install(Authentication) {
|
install(Authentication) {
|
||||||
session<AuthenticationSession> {
|
bearer {
|
||||||
|
authenticate { credential ->
|
||||||
|
SessionRepository.by(credential.token)?.principal
|
||||||
|
}
|
||||||
|
}
|
||||||
|
session<Session> {
|
||||||
validate { session ->
|
validate { session ->
|
||||||
val session = SessionRepository
|
SessionRepository.by(session.token)?.principal
|
||||||
.singleOrNull { SessionTable.tokenHash eq Hasher.hash(session.token) }
|
?.also { sessions.set(session) }
|
||||||
?: return@validate null
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sessions.set(session)
|
val SessionEntity.principal: Principal
|
||||||
|
get() = Principal(account(), this)
|
||||||
AuthenticationPrincipal(session.account(), session)
|
|
||||||
}
|
|
||||||
challenge {
|
|
||||||
call.sessions.clear<AuthenticationSession>()
|
|
||||||
|
|
||||||
throw UnauthorizedException()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
package com.infendro.account.config
|
||||||
|
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import io.ktor.server.sessions.*
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlin.time.Duration.Companion.days
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class Session(
|
||||||
|
val token: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
fun Application.configureSession() {
|
||||||
|
val config = configuration
|
||||||
|
|
||||||
|
install(Sessions) {
|
||||||
|
cookie<Session>("Authentication") {
|
||||||
|
this.cookie.domain = config.security.cookie.domain
|
||||||
|
this.cookie.path = config.security.cookie.path
|
||||||
|
this.cookie.httpOnly = true
|
||||||
|
this.cookie.secure = config.security.cookie.secure
|
||||||
|
this.cookie.extensions["SameSite"] = "Strict"
|
||||||
|
this.cookie.maxAge = 7.days
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,12 +18,10 @@ data class PostAccountRequest(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun RequestValidationConfig.validatePostAccountRequest() {
|
fun RequestValidationConfig.validatePostAccountRequest() = validate<PostAccountRequest> { request ->
|
||||||
validate<PostAccountRequest> { request ->
|
|
||||||
when {
|
when {
|
||||||
!USERNAME.matches(request.username) -> Invalid("")
|
!USERNAME.matches(request.username) -> Invalid("")
|
||||||
!PASSWORD.matches(request.password) -> Invalid("")
|
!PASSWORD.matches(request.password) -> Invalid("")
|
||||||
else -> Valid
|
else -> Valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ data class PutAccountCurrentPasswordRequest(
|
|||||||
val otp: String,
|
val otp: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun RequestValidationConfig.validatePutAccountCurrentPasswordRequest() {
|
fun RequestValidationConfig.validatePutAccountCurrentPasswordRequest() = validate<PutAccountCurrentPasswordRequest> { request ->
|
||||||
validate<PutAccountCurrentPasswordRequest> { request ->
|
|
||||||
when {
|
when {
|
||||||
!PASSWORD.matches(request.password) -> Invalid("")
|
!PASSWORD.matches(request.password) -> Invalid("")
|
||||||
else -> Valid
|
else -> Valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,11 +11,9 @@ data class PutAccountCurrentUsernameRequest(
|
|||||||
val otp: String,
|
val otp: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun RequestValidationConfig.validatePutAccountCurrentUsernameRequest() {
|
fun RequestValidationConfig.validatePutAccountCurrentUsernameRequest() = validate<PutAccountCurrentUsernameRequest> { request ->
|
||||||
validate<PutAccountCurrentUsernameRequest> { request ->
|
|
||||||
when {
|
when {
|
||||||
!USERNAME.matches(request.username) -> Invalid("")
|
!USERNAME.matches(request.username) -> Invalid("")
|
||||||
else -> Valid
|
else -> Valid
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,11 +13,9 @@ data class AccountResponse(
|
|||||||
val sessions: List<SessionResponse>,
|
val sessions: List<SessionResponse>,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun AccountEntity.toResponse(): AccountResponse {
|
fun AccountEntity.toResponse() = AccountResponse(
|
||||||
return AccountResponse(
|
|
||||||
id = id.value,
|
id = id.value,
|
||||||
username = username,
|
username = username,
|
||||||
role = role,
|
role = role,
|
||||||
sessions = sessions().map(SessionEntity::toResponse),
|
sessions = sessions().map(SessionEntity::toResponse),
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,9 +9,7 @@ data class SessionResponse(
|
|||||||
val accountId: Long,
|
val accountId: Long,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun SessionEntity.toResponse(): SessionResponse {
|
fun SessionEntity.toResponse() = SessionResponse(
|
||||||
return SessionResponse(
|
|
||||||
id = id.value,
|
id = id.value,
|
||||||
accountId = account().id.value,
|
accountId = account().id.value,
|
||||||
)
|
)
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import com.infendro.account.model.Role
|
||||||
|
import com.infendro.account.model.repository.AccountRepository
|
||||||
|
import com.infendro.account.util.PasswordHasher
|
||||||
|
import com.infendro.ktor.migration.MigrationConfig
|
||||||
|
|
||||||
|
fun MigrationConfig.initial() = migration("initial") {
|
||||||
|
exec(
|
||||||
|
"""
|
||||||
|
CREATE TABLE access
|
||||||
|
(
|
||||||
|
id BIGSERIAL
|
||||||
|
PRIMARY KEY,
|
||||||
|
token_hash TEXT
|
||||||
|
NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE account
|
||||||
|
(
|
||||||
|
id BIGSERIAL
|
||||||
|
PRIMARY KEY,
|
||||||
|
username TEXT
|
||||||
|
UNIQUE
|
||||||
|
NOT NULL,
|
||||||
|
password_hash TEXT
|
||||||
|
NOT NULL,
|
||||||
|
password_salt TEXT
|
||||||
|
NOT NULL,
|
||||||
|
secret TEXT
|
||||||
|
NOT NULL,
|
||||||
|
role TEXT
|
||||||
|
NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE session
|
||||||
|
(
|
||||||
|
id BIGSERIAL
|
||||||
|
PRIMARY KEY,
|
||||||
|
token_hash TEXT
|
||||||
|
NOT NULL,
|
||||||
|
account_id BIGINT
|
||||||
|
REFERENCES account (id) ON DELETE CASCADE
|
||||||
|
NOT NULL
|
||||||
|
);
|
||||||
|
""".trimIndent(),
|
||||||
|
)
|
||||||
|
|
||||||
|
AccountRepository.insert {
|
||||||
|
val salt = PasswordHasher.generateSalt()
|
||||||
|
|
||||||
|
this.username = "infendro"
|
||||||
|
this.passwordHash = PasswordHasher.hash("password", salt)
|
||||||
|
this.passwordSalt = salt
|
||||||
|
this.secret = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA===="
|
||||||
|
this.role = Role.OWNER
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,16 +13,5 @@ enum class Role(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
infix fun List<Role>.except(
|
infix fun List<Role>.except(roles: List<Role>) = filterNot { it in roles }
|
||||||
roles: List<Role>,
|
infix fun List<Role>.except(role: Role) = filterNot { it == role }
|
||||||
): List<Role> {
|
|
||||||
return filterNot { it in roles }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|
||||||
infix fun List<Role>.except(
|
|
||||||
role: Role,
|
|
||||||
): List<Role> {
|
|
||||||
return filterNot { it == role }
|
|
||||||
.toList()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.infendro.account.model.entity
|
package com.infendro.account.model.entity
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.LongEntity
|
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.dao.LongEntityClass
|
import org.jetbrains.exposed.v1.core.dao.id.LongIdTable
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.v1.dao.LongEntity
|
||||||
import org.jetbrains.exposed.dao.id.LongIdTable
|
import org.jetbrains.exposed.v1.dao.LongEntityClass
|
||||||
|
|
||||||
object AccessTable : LongIdTable("access", "id") {
|
object AccessTable : LongIdTable("access", "id") {
|
||||||
val tokenHash = text("token_hash")
|
val tokenHash = text("token_hash")
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
package com.infendro.account.model.entity
|
package com.infendro.account.model.entity
|
||||||
|
|
||||||
import com.infendro.account.model.Role
|
import com.infendro.account.model.Role
|
||||||
import org.jetbrains.exposed.dao.LongEntity
|
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.dao.LongEntityClass
|
import org.jetbrains.exposed.v1.core.dao.id.LongIdTable
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.v1.dao.LongEntity
|
||||||
import org.jetbrains.exposed.dao.id.LongIdTable
|
import org.jetbrains.exposed.v1.dao.LongEntityClass
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
|
||||||
object AccountTable : LongIdTable("account", "id") {
|
object AccountTable : LongIdTable("account", "id") {
|
||||||
val username = text("username")
|
val username = text("username")
|
||||||
@@ -20,7 +20,7 @@ object AccountTable : LongIdTable("account", "id") {
|
|||||||
else -> throw Error()
|
else -> throw Error()
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toDb = { it.name }
|
toDb = { it.name },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package com.infendro.account.model.entity
|
package com.infendro.account.model.entity
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.LongEntity
|
import org.jetbrains.exposed.v1.core.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.dao.LongEntityClass
|
import org.jetbrains.exposed.v1.core.dao.id.LongIdTable
|
||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.v1.dao.LongEntity
|
||||||
import org.jetbrains.exposed.dao.id.LongIdTable
|
import org.jetbrains.exposed.v1.dao.LongEntityClass
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
|
||||||
object SessionTable : LongIdTable("session", "id") {
|
object SessionTable : LongIdTable("session", "id") {
|
||||||
val tokenHash = text("token_hash")
|
val tokenHash = text("token_hash")
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
package com.infendro.account.model.repository
|
package com.infendro.account.model.repository
|
||||||
|
|
||||||
import org.jetbrains.exposed.dao.LongEntity
|
import org.jetbrains.exposed.v1.core.Op
|
||||||
import org.jetbrains.exposed.dao.LongEntityClass
|
import org.jetbrains.exposed.v1.dao.LongEntity
|
||||||
import org.jetbrains.exposed.sql.Op
|
import org.jetbrains.exposed.v1.dao.LongEntityClass
|
||||||
import org.jetbrains.exposed.sql.transactions.transaction
|
import org.jetbrains.exposed.v1.jdbc.transactions.transaction
|
||||||
|
|
||||||
abstract class Repository<ENTITY : LongEntity>(
|
abstract class Repository<ENTITY : LongEntity>(
|
||||||
private val entityClass: LongEntityClass<ENTITY>,
|
private val entityClass: LongEntityClass<ENTITY>,
|
||||||
@@ -12,113 +12,75 @@ abstract class Repository<ENTITY : LongEntity>(
|
|||||||
entityClass.all().toList()
|
entityClass.all().toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun all(
|
fun all(where: Op<Boolean>): List<ENTITY> = transaction {
|
||||||
where: Op<Boolean>,
|
|
||||||
): List<ENTITY> = transaction {
|
|
||||||
entityClass.find(where).toList()
|
entityClass.find(where).toList()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun all(
|
fun all(where: () -> Op<Boolean>): List<ENTITY> {
|
||||||
where: () -> Op<Boolean>,
|
|
||||||
): List<ENTITY> {
|
|
||||||
return all(where())
|
return all(where())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun single(
|
fun single(where: Op<Boolean>): ENTITY = transaction {
|
||||||
where: Op<Boolean>,
|
|
||||||
): ENTITY = transaction {
|
|
||||||
entityClass.find(where).single()
|
entityClass.find(where).single()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun single(
|
fun single(where: () -> Op<Boolean>): ENTITY {
|
||||||
where: () -> Op<Boolean>,
|
|
||||||
): ENTITY {
|
|
||||||
return single(where())
|
return single(where())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun singleOrNull(
|
fun singleOrNull(where: Op<Boolean>): ENTITY? = transaction {
|
||||||
where: Op<Boolean>,
|
|
||||||
): ENTITY? = transaction {
|
|
||||||
entityClass.find(where).singleOrNull()
|
entityClass.find(where).singleOrNull()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun singleOrNull(
|
fun singleOrNull(where: () -> Op<Boolean>): ENTITY? {
|
||||||
where: () -> Op<Boolean>,
|
|
||||||
): ENTITY? {
|
|
||||||
return singleOrNull(where())
|
return singleOrNull(where())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exists(
|
fun exists(where: Op<Boolean>): Boolean = transaction {
|
||||||
where: Op<Boolean>,
|
|
||||||
): Boolean = transaction {
|
|
||||||
entityClass.find(where).any()
|
entityClass.find(where).any()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun exists(
|
fun exists(where: () -> Op<Boolean>): Boolean {
|
||||||
where: () -> Op<Boolean>,
|
|
||||||
): Boolean {
|
|
||||||
return exists(where())
|
return exists(where())
|
||||||
}
|
}
|
||||||
|
|
||||||
fun insert(
|
fun insert(block: ENTITY.() -> Unit): Unit = transaction {
|
||||||
block: ENTITY.() -> Unit,
|
|
||||||
): Unit = transaction {
|
|
||||||
entityClass.new(block)
|
entityClass.new(block)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(
|
fun update(entity: ENTITY, block: ENTITY.() -> Unit): Unit = transaction {
|
||||||
entity: ENTITY,
|
|
||||||
block: ENTITY.() -> Unit,
|
|
||||||
): Unit = transaction {
|
|
||||||
entity.block()
|
entity.block()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(
|
fun update(entities: Iterable<ENTITY>, block: ENTITY.() -> Unit) {
|
||||||
entities: Iterable<ENTITY>,
|
|
||||||
block: ENTITY.() -> Unit,
|
|
||||||
) {
|
|
||||||
for (entity in entities) {
|
for (entity in entities) {
|
||||||
update(entity, block)
|
update(entity, block)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(
|
fun update(where: Op<Boolean>, block: ENTITY.() -> Unit) {
|
||||||
where: Op<Boolean>,
|
|
||||||
block: ENTITY.() -> Unit,
|
|
||||||
) {
|
|
||||||
update(all(where), block)
|
update(all(where), block)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun update(
|
fun update(where: () -> Op<Boolean>, block: ENTITY.() -> Unit) {
|
||||||
where: () -> Op<Boolean>,
|
|
||||||
block: ENTITY.() -> Unit,
|
|
||||||
) {
|
|
||||||
update(where(), block)
|
update(where(), block)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(
|
fun delete(entity: ENTITY): Unit = transaction {
|
||||||
entity: ENTITY,
|
|
||||||
): Unit = transaction {
|
|
||||||
entity.delete()
|
entity.delete()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(
|
fun delete(entities: Iterable<ENTITY>) {
|
||||||
entities: Iterable<ENTITY>,
|
|
||||||
) {
|
|
||||||
for (entity in entities) {
|
for (entity in entities) {
|
||||||
delete(entity)
|
delete(entity)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(
|
fun delete(where: Op<Boolean>) {
|
||||||
where: Op<Boolean>,
|
|
||||||
) {
|
|
||||||
delete(all(where))
|
delete(all(where))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delete(
|
fun delete(where: () -> Op<Boolean>) {
|
||||||
where: () -> Op<Boolean>,
|
|
||||||
) {
|
|
||||||
delete(where())
|
delete(where())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,10 @@
|
|||||||
package com.infendro.account.model.repository
|
package com.infendro.account.model.repository
|
||||||
|
|
||||||
import com.infendro.account.model.entity.SessionEntity
|
import com.infendro.account.model.entity.SessionEntity
|
||||||
|
import com.infendro.account.model.entity.SessionTable
|
||||||
|
import com.infendro.account.util.Hasher
|
||||||
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
|
||||||
object SessionRepository : Repository<SessionEntity>(SessionEntity)
|
object SessionRepository : Repository<SessionEntity>(SessionEntity) {
|
||||||
|
fun by(token: String) = singleOrNull { SessionTable.tokenHash eq Hasher.hash(token) }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.infendro.account.routing
|
package com.infendro.account.routing
|
||||||
|
|
||||||
import com.infendro.account.config.AuthenticationPrincipal
|
import com.infendro.account.config.Principal
|
||||||
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.*
|
import io.ktor.resources.*
|
||||||
@@ -27,21 +27,21 @@ private class Access {
|
|||||||
fun Routing.access() {
|
fun Routing.access() {
|
||||||
authenticate {
|
authenticate {
|
||||||
post<Access> {
|
post<Access> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
|
|
||||||
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<Principal>()!!
|
||||||
|
|
||||||
AccessService.getAll(principal)
|
AccessService.getAll(principal)
|
||||||
.also { call.respond(it) }
|
.also { call.respond(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
delete<Access.Id> { resource ->
|
delete<Access.Id> { resource ->
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
val id = resource.id
|
val id = resource.id
|
||||||
|
|
||||||
AccessService.deleteId(principal, id)
|
AccessService.deleteId(principal, id)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.infendro.account.routing
|
package com.infendro.account.routing
|
||||||
|
|
||||||
import com.infendro.account.config.AuthenticationPrincipal
|
import com.infendro.account.config.Principal
|
||||||
import com.infendro.account.dto.request.*
|
import com.infendro.account.dto.request.*
|
||||||
import com.infendro.account.service.AccountService
|
import com.infendro.account.service.AccountService
|
||||||
import io.ktor.http.HttpStatusCode.Companion.OK
|
import io.ktor.http.HttpStatusCode.Companion.OK
|
||||||
@@ -79,28 +79,28 @@ fun Routing.account() {
|
|||||||
|
|
||||||
authenticate {
|
authenticate {
|
||||||
get<Account.All> {
|
get<Account.All> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
|
|
||||||
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<Principal>()!!
|
||||||
|
|
||||||
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<Principal>()!!
|
||||||
|
|
||||||
AccountService.deleteCurrent(principal)
|
AccountService.deleteCurrent(principal)
|
||||||
call.respond(OK)
|
call.respond(OK)
|
||||||
}
|
}
|
||||||
|
|
||||||
put<Account.Current.Username> {
|
put<Account.Current.Username> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
val request = call.receive<PutAccountCurrentUsernameRequest>()
|
val request = call.receive<PutAccountCurrentUsernameRequest>()
|
||||||
|
|
||||||
AccountService.putCurrentUsername(principal, call.sessions, request)
|
AccountService.putCurrentUsername(principal, call.sessions, request)
|
||||||
@@ -108,7 +108,7 @@ fun Routing.account() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
put<Account.Current.Password> {
|
put<Account.Current.Password> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
val request = call.receive<PutAccountCurrentPasswordRequest>()
|
val request = call.receive<PutAccountCurrentPasswordRequest>()
|
||||||
|
|
||||||
AccountService.putCurrentPassword(principal, call.sessions, request)
|
AccountService.putCurrentPassword(principal, call.sessions, request)
|
||||||
@@ -116,7 +116,7 @@ fun Routing.account() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
post<Account.Current.Secret> {
|
post<Account.Current.Secret> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
val request = call.receive<PostAccountCurrentSecretRequest>()
|
val request = call.receive<PostAccountCurrentSecretRequest>()
|
||||||
|
|
||||||
AccountService.postCurrentSecret(principal, call.sessions, request)
|
AccountService.postCurrentSecret(principal, call.sessions, request)
|
||||||
@@ -124,14 +124,14 @@ fun Routing.account() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get<Account.Current.Session.All> {
|
get<Account.Current.Session.All> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
|
|
||||||
AccountService.getCurrentSessionAll(principal)
|
AccountService.getCurrentSessionAll(principal)
|
||||||
.also { call.respond(it) }
|
.also { call.respond(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
delete<Account.Current.Session.Id> { resource ->
|
delete<Account.Current.Session.Id> { resource ->
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
val id = resource.id
|
val id = resource.id
|
||||||
|
|
||||||
AccountService.deleteCurrentSessionId(principal, call.sessions, id)
|
AccountService.deleteCurrentSessionId(principal, call.sessions, id)
|
||||||
@@ -139,7 +139,7 @@ fun Routing.account() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete<Account.Id> { resource ->
|
delete<Account.Id> { resource ->
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
val id = resource.id
|
val id = resource.id
|
||||||
|
|
||||||
AccountService.deleteId(principal, id)
|
AccountService.deleteId(principal, id)
|
||||||
@@ -147,7 +147,7 @@ fun Routing.account() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
put<Account.Id.Role> { resource ->
|
put<Account.Id.Role> { resource ->
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
val id = resource.parent.id
|
val id = resource.parent.id
|
||||||
val request = call.receive<PutAccountIdRoleRequest>()
|
val request = call.receive<PutAccountIdRoleRequest>()
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.infendro.account.routing
|
package com.infendro.account.routing
|
||||||
|
|
||||||
import com.infendro.account.config.AuthenticationPrincipal
|
import com.infendro.account.config.Principal
|
||||||
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
|
||||||
@@ -42,14 +42,14 @@ fun Routing.session() {
|
|||||||
|
|
||||||
authenticate {
|
authenticate {
|
||||||
get<Session.All> {
|
get<Session.All> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
|
|
||||||
SessionService.getAll(principal)
|
SessionService.getAll(principal)
|
||||||
.also { call.respond(it) }
|
.also { call.respond(it) }
|
||||||
}
|
}
|
||||||
|
|
||||||
delete<Session.Id> { resource ->
|
delete<Session.Id> { resource ->
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
val id = resource.id
|
val id = resource.id
|
||||||
|
|
||||||
SessionService.deleteId(principal, id)
|
SessionService.deleteId(principal, id)
|
||||||
@@ -57,14 +57,14 @@ fun Routing.session() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
get<Session.Current> {
|
get<Session.Current> {
|
||||||
val principal = call.principal<AuthenticationPrincipal>()!!
|
val principal = call.principal<Principal>()!!
|
||||||
|
|
||||||
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<Principal>()!!
|
||||||
|
|
||||||
SessionService.deleteCurrent(principal, call.sessions)
|
SessionService.deleteCurrent(principal, call.sessions)
|
||||||
call.respond(OK)
|
call.respond(OK)
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.infendro.account.service
|
package com.infendro.account.service
|
||||||
|
|
||||||
import com.infendro.account.config.AuthenticationPrincipal
|
import com.infendro.account.config.Principal
|
||||||
import com.infendro.account.dto.response.AccessResponse
|
import com.infendro.account.dto.response.AccessResponse
|
||||||
import com.infendro.account.dto.response.PostAccessResponse
|
import com.infendro.account.dto.response.PostAccessResponse
|
||||||
import com.infendro.account.dto.response.toResponse
|
import com.infendro.account.dto.response.toResponse
|
||||||
@@ -11,12 +11,10 @@ import com.infendro.account.model.entity.AccessTable
|
|||||||
import com.infendro.account.model.repository.AccessRepository
|
import com.infendro.account.model.repository.AccessRepository
|
||||||
import com.infendro.account.util.Hasher
|
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.v1.core.eq
|
||||||
|
|
||||||
object AccessService {
|
object AccessService {
|
||||||
fun post(
|
fun post(principal: Principal): PostAccessResponse {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
): PostAccessResponse {
|
|
||||||
if (principal.role != OWNER)
|
if (principal.role != OWNER)
|
||||||
throw ForbiddenException()
|
throw ForbiddenException()
|
||||||
|
|
||||||
@@ -26,13 +24,11 @@ object AccessService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return PostAccessResponse(
|
return PostAccessResponse(
|
||||||
token = token
|
token = token,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll(
|
fun getAll(principal: Principal): List<AccessResponse> {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
): List<AccessResponse> {
|
|
||||||
if (principal.role != OWNER)
|
if (principal.role != OWNER)
|
||||||
throw ForbiddenException()
|
throw ForbiddenException()
|
||||||
|
|
||||||
@@ -41,10 +37,7 @@ object AccessService {
|
|||||||
.map { it.toResponse() }
|
.map { it.toResponse() }
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteId(
|
fun deleteId(principal: Principal, id: Long) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
id: Long,
|
|
||||||
) {
|
|
||||||
if (principal.role != OWNER)
|
if (principal.role != OWNER)
|
||||||
throw ForbiddenException()
|
throw ForbiddenException()
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.infendro.account.service
|
package com.infendro.account.service
|
||||||
|
|
||||||
import com.infendro.account.config.AuthenticationPrincipal
|
import com.infendro.account.config.Principal
|
||||||
import com.infendro.account.config.AuthenticationSession
|
import com.infendro.account.config.Session
|
||||||
import com.infendro.account.dto.request.*
|
import com.infendro.account.dto.request.*
|
||||||
import com.infendro.account.dto.response.*
|
import com.infendro.account.dto.response.*
|
||||||
import com.infendro.account.exception.client.ConflictException
|
import com.infendro.account.exception.client.ConflictException
|
||||||
@@ -16,14 +16,12 @@ import com.infendro.account.model.repository.AccountRepository
|
|||||||
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 com.infendro.account.util.OTP
|
import com.infendro.account.util.OTP
|
||||||
import com.infendro.account.util.SecureHasher
|
import com.infendro.account.util.PasswordHasher
|
||||||
import io.ktor.server.sessions.*
|
import io.ktor.server.sessions.*
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
|
|
||||||
object AccountService {
|
object AccountService {
|
||||||
fun post(
|
fun post(request: PostAccountRequest): PostAccountResponse {
|
||||||
request: PostAccountRequest,
|
|
||||||
): 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()
|
||||||
@@ -36,23 +34,21 @@ object AccountService {
|
|||||||
|
|
||||||
val secret = OTP.generateSecret()
|
val secret = OTP.generateSecret()
|
||||||
AccountRepository.insert {
|
AccountRepository.insert {
|
||||||
val salt = SecureHasher.generateSalt()
|
val salt = PasswordHasher.generateSalt()
|
||||||
|
|
||||||
this.username = request.username
|
this.username = request.username
|
||||||
this.passwordHash = SecureHasher.hash(request.password, salt)
|
this.passwordHash = PasswordHasher.hash(request.password, salt)
|
||||||
this.passwordSalt = salt
|
this.passwordSalt = salt
|
||||||
this.secret = secret
|
this.secret = secret
|
||||||
this.role = USER
|
this.role = USER
|
||||||
}
|
}
|
||||||
|
|
||||||
return PostAccountResponse(
|
return PostAccountResponse(
|
||||||
secret = secret
|
secret = secret,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll(
|
fun getAll(principal: Principal): List<AccountResponse> {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
): List<AccountResponse> {
|
|
||||||
if (principal.role != OWNER)
|
if (principal.role != OWNER)
|
||||||
throw ForbiddenException()
|
throw ForbiddenException()
|
||||||
|
|
||||||
@@ -61,26 +57,18 @@ object AccountService {
|
|||||||
.map(AccountEntity::toResponse)
|
.map(AccountEntity::toResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrent(
|
fun getCurrent(principal: Principal): AccountResponse {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
): AccountResponse {
|
|
||||||
return principal.account.toResponse()
|
return principal.account.toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCurrent(
|
fun deleteCurrent(principal: Principal) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
) {
|
|
||||||
if (principal.role == OWNER)
|
if (principal.role == OWNER)
|
||||||
throw ForbiddenException()
|
throw ForbiddenException()
|
||||||
|
|
||||||
AccountRepository.delete(principal.account)
|
AccountRepository.delete(principal.account)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putCurrentUsername(
|
fun putCurrentUsername(principal: Principal, sessions: CurrentSession, request: PutAccountCurrentUsernameRequest) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
sessions: CurrentSession,
|
|
||||||
request: PutAccountCurrentUsernameRequest,
|
|
||||||
) {
|
|
||||||
if (!OTP.verify(principal.account.secret, request.otp))
|
if (!OTP.verify(principal.account.secret, request.otp))
|
||||||
throw UnauthorizedException()
|
throw UnauthorizedException()
|
||||||
|
|
||||||
@@ -89,34 +77,30 @@ object AccountService {
|
|||||||
}
|
}
|
||||||
SessionRepository.delete(principal.account.sessions())
|
SessionRepository.delete(principal.account.sessions())
|
||||||
|
|
||||||
sessions.clear<AuthenticationSession>()
|
sessions.clear<Session>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putCurrentPassword(
|
fun putCurrentPassword(principal: Principal, sessions: CurrentSession, request: PutAccountCurrentPasswordRequest) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
sessions: CurrentSession,
|
|
||||||
request: PutAccountCurrentPasswordRequest,
|
|
||||||
) {
|
|
||||||
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 = PasswordHasher.generateSalt()
|
||||||
|
|
||||||
this.passwordHash = SecureHasher.hash(request.password, salt)
|
this.passwordHash = PasswordHasher.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<Session>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun postCurrentSecret(
|
fun postCurrentSecret(
|
||||||
principal: AuthenticationPrincipal,
|
principal: Principal,
|
||||||
sessions: CurrentSession,
|
sessions: CurrentSession,
|
||||||
request: PostAccountCurrentSecretRequest,
|
request: PostAccountCurrentSecretRequest
|
||||||
): PostAccountCurrentSecretResponse {
|
): PostAccountCurrentSecretResponse {
|
||||||
if (principal.account.passwordHash != SecureHasher.hash(request.password, principal.account.passwordSalt))
|
if (principal.account.passwordHash != PasswordHasher.hash(request.password, principal.account.passwordSalt))
|
||||||
throw UnauthorizedException()
|
throw UnauthorizedException()
|
||||||
|
|
||||||
val secret = OTP.generateSecret()
|
val secret = OTP.generateSecret()
|
||||||
@@ -125,25 +109,19 @@ object AccountService {
|
|||||||
}
|
}
|
||||||
SessionRepository.delete(principal.account.sessions())
|
SessionRepository.delete(principal.account.sessions())
|
||||||
|
|
||||||
sessions.clear<AuthenticationSession>()
|
sessions.clear<Session>()
|
||||||
|
|
||||||
return PostAccountCurrentSecretResponse(
|
return PostAccountCurrentSecretResponse(
|
||||||
secret = secret
|
secret = secret,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrentSessionAll(
|
fun getCurrentSessionAll(principal: Principal): List<SessionResponse> {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
): List<SessionResponse> {
|
|
||||||
return principal.account.sessions()
|
return principal.account.sessions()
|
||||||
.map(SessionEntity::toResponse)
|
.map(SessionEntity::toResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCurrentSessionId(
|
fun deleteCurrentSessionId(principal: Principal, sessions: CurrentSession, id: Long) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
sessions: CurrentSession,
|
|
||||||
id: Long,
|
|
||||||
) {
|
|
||||||
val session = SessionRepository
|
val session = SessionRepository
|
||||||
.singleOrNull { SessionTable.id eq id }
|
.singleOrNull { SessionTable.id eq id }
|
||||||
?: throw NotFoundException()
|
?: throw NotFoundException()
|
||||||
@@ -154,13 +132,10 @@ object AccountService {
|
|||||||
SessionRepository.delete(session)
|
SessionRepository.delete(session)
|
||||||
|
|
||||||
if (session.id == principal.session.id)
|
if (session.id == principal.session.id)
|
||||||
sessions.clear<AuthenticationSession>()
|
sessions.clear<Session>()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteId(
|
fun deleteId(principal: Principal, id: Long) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
id: Long,
|
|
||||||
) {
|
|
||||||
val account = AccountRepository
|
val account = AccountRepository
|
||||||
.singleOrNull { AccountTable.id eq id }
|
.singleOrNull { AccountTable.id eq id }
|
||||||
?: throw NotFoundException()
|
?: throw NotFoundException()
|
||||||
@@ -171,11 +146,7 @@ object AccountService {
|
|||||||
AccountRepository.delete(account)
|
AccountRepository.delete(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun putIdRole(
|
fun putIdRole(principal: Principal, id: Long, request: PutAccountIdRoleRequest) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
id: Long,
|
|
||||||
request: PutAccountIdRoleRequest,
|
|
||||||
) {
|
|
||||||
val account = AccountRepository
|
val account = AccountRepository
|
||||||
.singleOrNull { AccountTable.id eq id }
|
.singleOrNull { AccountTable.id eq id }
|
||||||
?: throw NotFoundException()
|
?: throw NotFoundException()
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.infendro.account.service
|
package com.infendro.account.service
|
||||||
|
|
||||||
import com.infendro.account.config.AuthenticationPrincipal
|
import com.infendro.account.config.Principal
|
||||||
import com.infendro.account.config.AuthenticationSession
|
import com.infendro.account.config.Session
|
||||||
import com.infendro.account.dto.request.PostSessionRequest
|
import com.infendro.account.dto.request.PostSessionRequest
|
||||||
import com.infendro.account.dto.response.SessionResponse
|
import com.infendro.account.dto.response.SessionResponse
|
||||||
import com.infendro.account.dto.response.toResponse
|
import com.infendro.account.dto.response.toResponse
|
||||||
@@ -16,17 +16,14 @@ import com.infendro.account.model.repository.AccountRepository
|
|||||||
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 com.infendro.account.util.OTP
|
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 com.infendro.account.util.TokenGenerator
|
||||||
import io.ktor.server.sessions.*
|
import io.ktor.server.sessions.*
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
import org.jetbrains.exposed.v1.core.eq
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.inList
|
import org.jetbrains.exposed.v1.core.inList
|
||||||
|
|
||||||
object SessionService {
|
object SessionService {
|
||||||
fun post(
|
fun post(sessions: CurrentSession, request: PostSessionRequest) {
|
||||||
sessions: CurrentSession,
|
|
||||||
request: PostSessionRequest,
|
|
||||||
) {
|
|
||||||
val account = AccountRepository
|
val account = AccountRepository
|
||||||
.singleOrNull { AccountTable.username eq request.username }
|
.singleOrNull { AccountTable.username eq request.username }
|
||||||
?: throw NotFoundException()
|
?: throw NotFoundException()
|
||||||
@@ -34,7 +31,7 @@ object SessionService {
|
|||||||
if (!OTP.verify(account.secret, request.otp))
|
if (!OTP.verify(account.secret, request.otp))
|
||||||
throw UnauthorizedException()
|
throw UnauthorizedException()
|
||||||
|
|
||||||
if (account.passwordHash != SecureHasher.hash(request.password, account.passwordSalt))
|
if (account.passwordHash != PasswordHasher.hash(request.password, account.passwordSalt))
|
||||||
throw UnauthorizedException()
|
throw UnauthorizedException()
|
||||||
|
|
||||||
val token = TokenGenerator.generate()
|
val token = TokenGenerator.generate()
|
||||||
@@ -43,14 +40,10 @@ object SessionService {
|
|||||||
this.account = account
|
this.account = account
|
||||||
}
|
}
|
||||||
|
|
||||||
sessions.set(
|
sessions.set(Session(token))
|
||||||
AuthenticationSession(token)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getAll(
|
fun getAll(principal: Principal): List<SessionResponse> {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
): List<SessionResponse> {
|
|
||||||
if (principal.role == USER)
|
if (principal.role == USER)
|
||||||
throw ForbiddenException()
|
throw ForbiddenException()
|
||||||
|
|
||||||
@@ -60,10 +53,7 @@ object SessionService {
|
|||||||
.map(SessionEntity::toResponse)
|
.map(SessionEntity::toResponse)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteId(
|
fun deleteId(principal: Principal, id: Long) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
id: Long,
|
|
||||||
) {
|
|
||||||
if (principal.role == USER)
|
if (principal.role == USER)
|
||||||
throw ForbiddenException()
|
throw ForbiddenException()
|
||||||
|
|
||||||
@@ -77,18 +67,13 @@ object SessionService {
|
|||||||
SessionRepository.delete(session)
|
SessionRepository.delete(session)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getCurrent(
|
fun getCurrent(principal: Principal): SessionResponse {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
): SessionResponse {
|
|
||||||
return principal.session.toResponse()
|
return principal.session.toResponse()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun deleteCurrent(
|
fun deleteCurrent(principal: Principal, sessions: CurrentSession) {
|
||||||
principal: AuthenticationPrincipal,
|
|
||||||
sessions: CurrentSession,
|
|
||||||
) {
|
|
||||||
SessionRepository.delete(principal.session)
|
SessionRepository.delete(principal.session)
|
||||||
|
|
||||||
sessions.clear<AuthenticationSession>()
|
sessions.clear<Session>()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
package com.infendro.account.util
|
package com.infendro.account.util
|
||||||
|
|
||||||
import com.infendro.encoding.Hex
|
import com.infendro.encoding.Base16
|
||||||
import com.infendro.hash.SHA256
|
import com.infendro.hash.sha2.`SHA-256`
|
||||||
|
|
||||||
object Hasher {
|
object Hasher {
|
||||||
fun hash(
|
fun hash(value: String): String {
|
||||||
value: String,
|
val bytes = `SHA-256`.hash(value.encodeToByteArray())
|
||||||
): String {
|
return Base16.encode(bytes).decodeToString()
|
||||||
val bytes = SHA256.hash(value.toByteArray())
|
|
||||||
return Hex.encode(bytes).decodeToString()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,29 +1,27 @@
|
|||||||
package com.infendro.account.util
|
package com.infendro.account.util
|
||||||
|
|
||||||
import com.infendro.encoding.Base32
|
import com.infendro.encoding.Base32
|
||||||
import com.infendro.hash.SHA256
|
import com.infendro.hash.sha2.`SHA-256`
|
||||||
import com.infendro.otp.SecretGenerator
|
|
||||||
import com.infendro.otp.TOTP
|
import com.infendro.otp.TOTP
|
||||||
|
import com.infendro.random.csprng.`HMAC-DRBG`
|
||||||
import kotlin.time.Clock
|
import kotlin.time.Clock
|
||||||
import kotlin.time.Duration.Companion.seconds
|
import kotlin.time.Duration.Companion.seconds
|
||||||
|
|
||||||
object OTP {
|
object OTP {
|
||||||
|
private val random = `HMAC-DRBG`(`SHA-256`)
|
||||||
private val totp = TOTP(
|
private val totp = TOTP(
|
||||||
function = SHA256,
|
function = `SHA-256`,
|
||||||
length = 8,
|
length = 8,
|
||||||
period = 30.seconds,
|
period = 30.seconds,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun verify(
|
fun verify(secret: String, otp: String): Boolean {
|
||||||
secret: String,
|
val bytes = Base32.decode(secret.encodeToByteArray())
|
||||||
otp: String,
|
|
||||||
): Boolean {
|
|
||||||
val bytes = Base32.decode(secret.toByteArray())
|
|
||||||
return totp.verify(bytes, Clock.System.now(), otp)
|
return totp.verify(bytes, Clock.System.now(), otp)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun generateSecret(): String {
|
fun generateSecret(): String {
|
||||||
val bytes = SecretGenerator.generate(totp.hotp.function)
|
val bytes = random.nextBytes(32)
|
||||||
return Base32.encode(bytes).decodeToString()
|
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,26 +0,0 @@
|
|||||||
package com.infendro.account.util
|
|
||||||
|
|
||||||
import com.infendro.encoding.Hex
|
|
||||||
import com.infendro.hash.SHA256
|
|
||||||
import com.infendro.kdf.PBKDF2
|
|
||||||
import com.infendro.random.PRNG
|
|
||||||
|
|
||||||
object SecureHasher {
|
|
||||||
private val random = PRNG.HMAC
|
|
||||||
private val kdf = PBKDF2(SHA256)
|
|
||||||
|
|
||||||
fun hash(
|
|
||||||
value: String,
|
|
||||||
salt: String,
|
|
||||||
): String {
|
|
||||||
val value = value.encodeToByteArray()
|
|
||||||
val salt = Hex.decode(salt.encodeToByteArray())
|
|
||||||
return kdf.hash(value, salt, 100_000, 32)
|
|
||||||
.let { Hex.encode(it).decodeToString() }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun generateSalt(): String {
|
|
||||||
return random.nextBytes(16)
|
|
||||||
.let { Hex.encode(it).decodeToString() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
package com.infendro.account.util
|
package com.infendro.account.util
|
||||||
|
|
||||||
import com.infendro.encoding.Hex
|
import com.infendro.encoding.Base16
|
||||||
import com.infendro.random.PRNG
|
import com.infendro.hash.sha2.`SHA-256`
|
||||||
|
import com.infendro.random.csprng.`HMAC-DRBG`
|
||||||
|
|
||||||
object TokenGenerator {
|
object TokenGenerator {
|
||||||
private val random = PRNG.HMAC
|
private val random = `HMAC-DRBG`(`SHA-256`)
|
||||||
|
|
||||||
fun generate(): String {
|
fun generate(): String {
|
||||||
return random.nextBytes(32)
|
return random.nextBytes(32)
|
||||||
.let { Hex.encode(it).decodeToString() }
|
.let { Base16.encode(it).decodeToString() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user