implement migration
This commit is contained in:
@@ -30,8 +30,6 @@ dependencies {
|
|||||||
implementation(libs.exposed.dao)
|
implementation(libs.exposed.dao)
|
||||||
implementation(libs.exposed.jdbc)
|
implementation(libs.exposed.jdbc)
|
||||||
implementation(libs.postgres)
|
implementation(libs.postgres)
|
||||||
implementation(libs.flyway)
|
|
||||||
implementation(libs.flyway.postgres)
|
|
||||||
implementation(libs.koin)
|
implementation(libs.koin)
|
||||||
implementation(libs.koin.logger)
|
implementation(libs.koin.logger)
|
||||||
implementation(libs.random)
|
implementation(libs.random)
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
[versions]
|
[versions]
|
||||||
kotlin = "2.1.21"
|
kotlin = "2.2.10"
|
||||||
ktor = "3.1.3"
|
ktor = "3.1.3"
|
||||||
exposed = "0.60.0"
|
exposed = "0.60.0"
|
||||||
postgres = "42.7.5"
|
postgres = "42.7.5"
|
||||||
flyway = "11.7.0"
|
|
||||||
koin = "4.0.3"
|
koin = "4.0.3"
|
||||||
random = "1.0.0"
|
random = "1.0.0"
|
||||||
otp = "1.1.1"
|
otp = "1.1.1"
|
||||||
@@ -32,8 +31,6 @@ exposed = { module = "org.jetbrains.exposed:exposed-core", version.ref = "expose
|
|||||||
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" }
|
||||||
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
|
postgres = { module = "org.postgresql:postgresql", version.ref = "postgres" }
|
||||||
flyway = { module = "org.flywaydb:flyway-core", version.ref = "flyway" }
|
|
||||||
flyway-postgres = { module = "org.flywaydb:flyway-database-postgresql", version.ref = "flyway" }
|
|
||||||
|
|
||||||
koin = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
|
koin = { module = "io.insert-koin:koin-ktor", version.ref = "koin" }
|
||||||
koin-logger = { module = "io.insert-koin:koin-logger-slf4j", version.ref = "koin" }
|
koin-logger = { module = "io.insert-koin:koin-logger-slf4j", version.ref = "koin" }
|
||||||
|
|||||||
BIN
backend/gradle/wrapper/gradle-wrapper.jar
vendored
BIN
backend/gradle/wrapper/gradle-wrapper.jar
vendored
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
distributionBase=GRADLE_USER_HOME
|
distributionBase=GRADLE_USER_HOME
|
||||||
distributionPath=wrapper/dists
|
distributionPath=wrapper/dists
|
||||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip
|
distributionUrl=https\://services.gradle.org/distributions/gradle-8.14-bin.zip
|
||||||
networkTimeout=10000
|
networkTimeout=10000
|
||||||
validateDistributionUrl=true
|
validateDistributionUrl=true
|
||||||
zipStoreBase=GRADLE_USER_HOME
|
zipStoreBase=GRADLE_USER_HOME
|
||||||
|
|||||||
10
backend/gradlew
vendored
10
backend/gradlew
vendored
@@ -15,6 +15,8 @@
|
|||||||
# See the License for the specific language governing permissions and
|
# See the License for the specific language governing permissions and
|
||||||
# limitations under the License.
|
# limitations under the License.
|
||||||
#
|
#
|
||||||
|
# SPDX-License-Identifier: Apache-2.0
|
||||||
|
#
|
||||||
|
|
||||||
##############################################################################
|
##############################################################################
|
||||||
#
|
#
|
||||||
@@ -84,7 +86,7 @@ done
|
|||||||
# shellcheck disable=SC2034
|
# shellcheck disable=SC2034
|
||||||
APP_BASE_NAME=${0##*/}
|
APP_BASE_NAME=${0##*/}
|
||||||
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
|
||||||
APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
|
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
|
||||||
|
|
||||||
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
# Use the maximum available, or set MAX_FD != -1 to use that value.
|
||||||
MAX_FD=maximum
|
MAX_FD=maximum
|
||||||
@@ -112,7 +114,7 @@ case "$( uname )" in #(
|
|||||||
NONSTOP* ) nonstop=true ;;
|
NONSTOP* ) nonstop=true ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
|
CLASSPATH="\\\"\\\""
|
||||||
|
|
||||||
|
|
||||||
# Determine the Java command to use to start the JVM.
|
# Determine the Java command to use to start the JVM.
|
||||||
@@ -203,7 +205,7 @@ fi
|
|||||||
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
||||||
|
|
||||||
# Collect all arguments for the java command:
|
# Collect all arguments for the java command:
|
||||||
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
|
||||||
# and any embedded shellness will be escaped.
|
# and any embedded shellness will be escaped.
|
||||||
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
|
||||||
# treated as '${Hostname}' itself on the command line.
|
# treated as '${Hostname}' itself on the command line.
|
||||||
@@ -211,7 +213,7 @@ DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
|
|||||||
set -- \
|
set -- \
|
||||||
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
"-Dorg.gradle.appname=$APP_BASE_NAME" \
|
||||||
-classpath "$CLASSPATH" \
|
-classpath "$CLASSPATH" \
|
||||||
org.gradle.wrapper.GradleWrapperMain \
|
-jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
|
||||||
"$@"
|
"$@"
|
||||||
|
|
||||||
# Stop when "xargs" is not available.
|
# Stop when "xargs" is not available.
|
||||||
|
|||||||
6
backend/gradlew.bat
vendored
6
backend/gradlew.bat
vendored
@@ -13,6 +13,8 @@
|
|||||||
@rem See the License for the specific language governing permissions and
|
@rem See the License for the specific language governing permissions and
|
||||||
@rem limitations under the License.
|
@rem limitations under the License.
|
||||||
@rem
|
@rem
|
||||||
|
@rem SPDX-License-Identifier: Apache-2.0
|
||||||
|
@rem
|
||||||
|
|
||||||
@if "%DEBUG%"=="" @echo off
|
@if "%DEBUG%"=="" @echo off
|
||||||
@rem ##########################################################################
|
@rem ##########################################################################
|
||||||
@@ -68,11 +70,11 @@ goto fail
|
|||||||
:execute
|
:execute
|
||||||
@rem Setup the command line
|
@rem Setup the command line
|
||||||
|
|
||||||
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
|
set CLASSPATH=
|
||||||
|
|
||||||
|
|
||||||
@rem Execute Gradle
|
@rem Execute Gradle
|
||||||
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
|
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
|
||||||
|
|
||||||
:end
|
:end
|
||||||
@rem End local scope for the variables with windows NT shell
|
@rem End local scope for the variables with windows NT shell
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.infendro.account
|
package com.infendro.account
|
||||||
|
|
||||||
import com.infendro.account.config.*
|
import com.infendro.account.config.*
|
||||||
import com.infendro.account.model.initializeDatabase
|
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.cio.*
|
import io.ktor.server.cio.*
|
||||||
|
|
||||||
@@ -10,13 +9,14 @@ fun main(
|
|||||||
) = EngineMain.main(args)
|
) = EngineMain.main(args)
|
||||||
|
|
||||||
fun Application.module() {
|
fun Application.module() {
|
||||||
configureDatabase()
|
|
||||||
configureDependencyInjection()
|
configureDependencyInjection()
|
||||||
|
|
||||||
|
configureDatabase()
|
||||||
|
|
||||||
configureSecurity()
|
configureSecurity()
|
||||||
|
configureRouting()
|
||||||
|
|
||||||
configureSerialization()
|
configureSerialization()
|
||||||
configureValidation()
|
configureValidation()
|
||||||
configureRouting()
|
|
||||||
configureException()
|
configureException()
|
||||||
|
|
||||||
initializeDatabase()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
package com.infendro.account.config
|
package com.infendro.account.config
|
||||||
|
|
||||||
|
import com.infendro.account.model.migrate
|
||||||
import io.ktor.server.application.Application
|
import io.ktor.server.application.Application
|
||||||
import org.flywaydb.core.Flyway
|
|
||||||
import org.jetbrains.exposed.sql.Database
|
import org.jetbrains.exposed.sql.Database
|
||||||
|
|
||||||
fun Application.configureDatabase() {
|
fun Application.configureDatabase() {
|
||||||
@@ -12,9 +12,5 @@ fun Application.configureDatabase() {
|
|||||||
user = config.username,
|
user = config.username,
|
||||||
password = config.password,
|
password = config.password,
|
||||||
)
|
)
|
||||||
|
migrate()
|
||||||
val flyway = Flyway.configure()
|
|
||||||
.dataSource(config.url, config.username, config.password)
|
|
||||||
.load()
|
|
||||||
flyway.migrate()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ package com.infendro.account.config
|
|||||||
|
|
||||||
import com.infendro.account.model.repository.AccessRepository
|
import com.infendro.account.model.repository.AccessRepository
|
||||||
import com.infendro.account.model.repository.AccountRepository
|
import com.infendro.account.model.repository.AccountRepository
|
||||||
import com.infendro.account.model.repository.InitializationRepository
|
import com.infendro.account.model.repository.MigrationRepository
|
||||||
import com.infendro.account.model.repository.SessionRepository
|
import com.infendro.account.model.repository.SessionRepository
|
||||||
import com.infendro.account.service.AccessService
|
import com.infendro.account.service.AccessService
|
||||||
import com.infendro.account.service.AccountService
|
import com.infendro.account.service.AccountService
|
||||||
@@ -22,7 +22,7 @@ fun Application.configureDependencyInjection() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val module = module {
|
private val module = module {
|
||||||
singleOf(::InitializationRepository)
|
singleOf(::MigrationRepository)
|
||||||
singleOf(::AccessRepository)
|
singleOf(::AccessRepository)
|
||||||
singleOf(::AccountRepository)
|
singleOf(::AccountRepository)
|
||||||
singleOf(::SessionRepository)
|
singleOf(::SessionRepository)
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
package com.infendro.account.model
|
|
||||||
|
|
||||||
import com.infendro.account.model.entity.InitializationTable
|
|
||||||
import com.infendro.account.model.repository.AccountRepository
|
|
||||||
import com.infendro.account.model.repository.InitializationRepository
|
|
||||||
import com.infendro.account.util.SecureHasher
|
|
||||||
import io.ktor.server.application.Application
|
|
||||||
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
|
|
||||||
import org.koin.ktor.ext.inject
|
|
||||||
|
|
||||||
fun Application.initializeDatabase() {
|
|
||||||
owner()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Application.owner() = initialize("owner") {
|
|
||||||
val accountRepository by inject<AccountRepository>()
|
|
||||||
|
|
||||||
accountRepository.insert {
|
|
||||||
val salt = SecureHasher.generateSalt()
|
|
||||||
|
|
||||||
this.username = "infendro"
|
|
||||||
this.passwordHash = SecureHasher.hash("password", salt)
|
|
||||||
this.passwordSalt = salt
|
|
||||||
this.secret = "deadbeef"
|
|
||||||
this.role = Role.OWNER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Application.initialize(
|
|
||||||
name: String,
|
|
||||||
block: () -> Unit,
|
|
||||||
) {
|
|
||||||
val initializationRepository by inject<InitializationRepository>()
|
|
||||||
|
|
||||||
initializationRepository
|
|
||||||
.singleOrNull { InitializationTable.name eq name }
|
|
||||||
?.run { return }
|
|
||||||
|
|
||||||
block()
|
|
||||||
|
|
||||||
initializationRepository.insert {
|
|
||||||
this.name = name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
package com.infendro.account.model
|
||||||
|
|
||||||
|
import com.infendro.account.model.entity.MigrationTable
|
||||||
|
import com.infendro.account.model.repository.AccountRepository
|
||||||
|
import com.infendro.account.model.repository.MigrationRepository
|
||||||
|
import com.infendro.account.util.SecureHasher
|
||||||
|
import io.ktor.server.application.*
|
||||||
|
import org.intellij.lang.annotations.Language
|
||||||
|
import org.jetbrains.exposed.sql.SchemaUtils
|
||||||
|
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.migrate() {
|
||||||
|
transaction {
|
||||||
|
SchemaUtils.create(MigrationTable)
|
||||||
|
}
|
||||||
|
|
||||||
|
migration(
|
||||||
|
"initial",
|
||||||
|
"""
|
||||||
|
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()
|
||||||
|
)
|
||||||
|
migration("owner") {
|
||||||
|
val accounts by inject<AccountRepository>()
|
||||||
|
|
||||||
|
accounts.insert {
|
||||||
|
val salt = SecureHasher.generateSalt()
|
||||||
|
|
||||||
|
this.username = "infendro"
|
||||||
|
this.passwordHash = SecureHasher.hash("password", salt)
|
||||||
|
this.passwordSalt = salt
|
||||||
|
this.secret = "deadbeef"
|
||||||
|
this.role = Role.OWNER
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Application.migration(
|
||||||
|
name: String,
|
||||||
|
block: Transaction.() -> Unit,
|
||||||
|
) {
|
||||||
|
val migrations by inject<MigrationRepository>()
|
||||||
|
|
||||||
|
migrations
|
||||||
|
.singleOrNull { MigrationTable.name eq name }
|
||||||
|
?.run { return }
|
||||||
|
|
||||||
|
transaction {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
|
||||||
|
migrations.insert {
|
||||||
|
this.name = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Application.migration(
|
||||||
|
name: String,
|
||||||
|
@Language("sql") sql: String,
|
||||||
|
) = migration(name) {
|
||||||
|
exec(sql)
|
||||||
|
}
|
||||||
@@ -5,12 +5,12 @@ import org.jetbrains.exposed.dao.LongEntityClass
|
|||||||
import org.jetbrains.exposed.dao.id.EntityID
|
import org.jetbrains.exposed.dao.id.EntityID
|
||||||
import org.jetbrains.exposed.dao.id.LongIdTable
|
import org.jetbrains.exposed.dao.id.LongIdTable
|
||||||
|
|
||||||
object InitializationTable : LongIdTable("initialization", "id") {
|
object MigrationTable : LongIdTable("migration", "id") {
|
||||||
val name = text("name")
|
val name = text("name")
|
||||||
}
|
}
|
||||||
|
|
||||||
class InitializationEntity(id: EntityID<Long>) : LongEntity(id) {
|
class MigrationEntity(id: EntityID<Long>) : LongEntity(id) {
|
||||||
companion object : LongEntityClass<InitializationEntity>(InitializationTable)
|
companion object : LongEntityClass<MigrationEntity>(MigrationTable)
|
||||||
|
|
||||||
var name by InitializationTable.name
|
var name by MigrationTable.name
|
||||||
}
|
}
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
package com.infendro.account.model.repository
|
|
||||||
|
|
||||||
import com.infendro.account.model.entity.InitializationEntity
|
|
||||||
|
|
||||||
class InitializationRepository : Repository<InitializationEntity>(InitializationEntity)
|
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package com.infendro.account.model.repository
|
||||||
|
|
||||||
|
import com.infendro.account.model.entity.MigrationEntity
|
||||||
|
|
||||||
|
class MigrationRepository : Repository<MigrationEntity>(MigrationEntity)
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
CREATE TABLE initialization
|
|
||||||
(
|
|
||||||
id BIGSERIAL
|
|
||||||
PRIMARY KEY,
|
|
||||||
name TEXT
|
|
||||||
UNIQUE
|
|
||||||
NOT NULL
|
|
||||||
);
|
|
||||||
|
|
||||||
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
|
|
||||||
);
|
|
||||||
Reference in New Issue
Block a user