diff --git a/README.md b/README.md
index 936c1cc8..5ff1ef1b 100644
--- a/README.md
+++ b/README.md
@@ -23,33 +23,27 @@ If you want to host the application yourself, follow the instructions below:
### Environment
To run this application on your own host, you need to provide the following environment variables:
-- `TIMEMATES_RSOCKET_PORT` – The port on which rsocket instance will run (default: `8080`)
-- `TIMEMATES_DATABASE_URL` – The URL to the PostgreSQL database
-- `TIMEMATES_DATABASE_USER` – The username for the PostgreSQL database
-- `TIMEMATES_DATABASE_USER_PASSWORD` – The password for the PostgreSQL user
-- `TIMEMATES_FILES_PATH` – The path to the directory where uploaded files will be stored
-- `TIMEMATES_SMTP_HOST` – The SMTP host of the mailer
-- `TIMEMATES_SMTP_PORT` – The SMTP port of the mailer
-- `TIMEMATES_SMTP_USER` – The SMTP user of the mailer
-- `TIMEMATES_SMTP_USER_PASSWORD` – The password for the SMTP user
-- `TIMEMATES_SMTP_SENDER` – The email address of the SMTP mailer
-> **Note**
-> There are two mailer implementations available: SMTP and MailerSend. Depending on your choice, you need to provide the
-> corresponding environment variables.
->
-> If using the SMTP mailer implementation, make sure to set
-> the `TIMEMATES_SMTP_HOST`, `TIMEMATES_SMTP_PORT`, `TIMEMATES_SMTP_USER`, `TIMEMATES_SMTP_USER_PASSWORD` and
-> `TIMEMATES_SMTP_SENDER` variables.
->
-> If using the MailerSend implementation, you should set the `MAILERSEND_API_KEY`, `MAILERSEND_SENDER`, `MAILERSEND_CONFIRMATION_TEMPLATE` and `MAILERSEND_SUPPORT_EMAIL` variables.
->
-> Refer to the code documentation for more details on configuring the mailer implementation.
+- `timemates_rsocket_port` – The port on which the rsocket instance will run (default: `8080`)
+- `timemates_database_url` – The URL to the PostgreSQL database
+- `timemates_database_user` – The username for the PostgreSQL database
+- `timemates_database_password` – The password for the PostgreSQL user
+- `mailersend_api_key` – MailerSend API key
+- `mailersend_sender` – MailerSend sender
+- `mailersend_confirmation_template` – MailerSend template for authentication confirmation
+- `mailersend_support_email` – Support email for MailerSend
+- `timemates_timers_cache_size` – Cache size for timers (default: `100`)
+- `timemates_users_cache_size` – Cache size for users (default: `100`)
+- `timemates_auth_cache_size` – Cache size for authentication entities (default: `100`)
+- `timemates_auth_cache_alive` – Maximum alive time for authentication cache in seconds (default: `300` seconds, or 5
+ minutes)
+- `timemates_debug` – Enable debug mode (present or not, acts as a flag)
> **Note**
> You can also use Java arguments to set up the application. Refer to
> the [source code](/app/src/main/kotlin/io/timemates/backend/application/Application.kt) for more
> information.
+>
## Docker image
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 42fde0fc..adf7eb88 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -8,17 +8,23 @@ plugins {
}
dependencies {
- implementation(projects.data)
- implementation(projects.core)
- implementation(projects.common.smtpMailer)
- implementation(projects.common.cliArguments)
- implementation(projects.common.hashing)
+ implementation(projects.features.auth.domain)
+ implementation(projects.features.auth.dependencies)
+
+ implementation(projects.features.users.domain)
+ implementation(projects.features.users.dependencies)
+
+ implementation(projects.features.timers.domain)
+ implementation(projects.features.timers.dependencies)
+
+ implementation(projects.core.types)
- implementation(projects.infrastructure.rsocketApi)
- implementation(libs.ktor.client.core)
+ implementation(projects.foundation.time)
+ implementation(projects.foundation.random)
+ implementation(projects.foundation.cliArguments)
- implementation(libs.kotlinx.serialization.json)
+ implementation(projects.infrastructure.rsocketApi)
implementation(libs.kotlinx.coroutines)
implementation(libs.exposed.core)
@@ -27,18 +33,12 @@ dependencies {
implementation(libs.postgresql.driver)
implementation(libs.h2.database)
- implementation(libs.grpc.kotlin.stub)
-
- implementation(libs.grpc.netty)
- implementation(libs.grpc.services)
-
implementation(libs.koin.core)
-
implementation(libs.logback.classic)
}
application {
- mainClass.set("io.timemates.backend.application.ApplicationKt")
+ mainClass.set("org.timemates.backend.application.ApplicationKt")
}
diff --git a/app/src/main/kotlin/io/timemates/backend/application/Application.kt b/app/src/main/kotlin/io/timemates/backend/application/Application.kt
deleted file mode 100644
index 74e5c4ad..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/Application.kt
+++ /dev/null
@@ -1,141 +0,0 @@
-@file:Suppress("ExtractKtorModule")
-
-package io.timemates.backend.application
-
-import io.timemates.api.rsocket.startRSocketApi
-import io.timemates.backend.application.constants.ArgumentsConstants
-import io.timemates.backend.application.constants.EnvironmentConstants
-import io.timemates.backend.application.constants.FailureMessages
-import io.timemates.backend.application.dependencies.AppModule
-import io.timemates.backend.application.dependencies.configuration.DatabaseConfig
-import io.timemates.backend.application.dependencies.configuration.MailerConfiguration
-import io.timemates.backend.cli.getNamedIntOrNull
-import io.timemates.backend.cli.parseArguments
-import io.timemates.backend.data.common.repositories.MailerSendEmailsRepository
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.launch
-import org.koin.core.context.startKoin
-import org.koin.dsl.module
-
-/**
- * Application entry-point. Next environment variables should be provided:
- *
- * **Environment variables**:
- * - **TIMEMATES_RSOCKET_PORT** – The port on which RSocket instance will run (default: `8080`)
- * - **TIMEMATES_DATABASE_URL**: The URL of the database.
- * - **TIMEMATES_DATABASE_USER**: The username for the database connection.
- * - **TIMEMATES_DATABASE_USER_PASSWORD**: The password for the database connection.
- * - **TIMEMATES_SMTP_HOST**: The SMTP host for sending emails.
- * - **TIMEMATES_SMTP_PORT**: The SMTP port for sending emails.
- * - **TIMEMATES_SMTP_USER**: The username for the SMTP server.
- * - **TIMEMATES_SMTP_USER_PASSWORD**: The password for the SMTP server.
- * - **TIMEMATES_SMTP_SENDER_ADDRESS**: The sender email address for outgoing emails.
- * - **MAILERSEND_API_KEY**: The API key for MailerSend service.
- * - **MAILERSEND_SENDER**: The sender email address for MailerSend emails.
- * - **MAILERSEND_RECIPIENT**: The recipient email address for MailerSend emails.
- * - **MAILERSEND_CONFIRMATION_TEMPLATE**: The template ID for MailerSend confirmation emails.
- * - **TIMEMATES_FILES_PATH**: The path for file storage.
- *
- * **Program arguments**:
- *
- * Also, values above can be provided by arguments `grpcPort`, `rsocketPort`, `databaseUrl`, `databaseUser`
- * `databaseUserPassword` and `filesPath`.
- * For example: `java -jar timemates.jar -port 8080 -databaseUrl http..`
- *
- * **Arguments are used first, then environment variables as fallback.**
- * @see ArgumentsConstants
- * @see EnvironmentConstants
- */
-suspend fun main(args: Array): Unit = coroutineScope {
- val arguments = args.parseArguments()
-
- val rSocketPort = arguments.getNamedIntOrNull(ArgumentsConstants.RSOCKET_PORT)
- ?: System.getenv(EnvironmentConstants.RSOCKET_PORT)?.toIntOrNull()
- ?: 8080
-
- val databaseUrl = arguments.getNamedOrNull(ArgumentsConstants.DATABASE_URL)
- ?: System.getenv(EnvironmentConstants.DATABASE_URL)
- ?: error(FailureMessages.MISSING_DATABASE_URL)
-
- val databaseUser = arguments.getNamedOrNull(ArgumentsConstants.DATABASE_USER)
- ?: System.getenv(EnvironmentConstants.DATABASE_USER)
- ?: "".also { println("Database user was not specified, ignoring") }
-
- val databasePassword = arguments.getNamedOrNull(ArgumentsConstants.DATABASE_USER_PASSWORD)
- ?: System.getenv(EnvironmentConstants.DATABASE_USER_PASSWORD)
- ?: "".also { println("Database password was not specified, ignoring") }
-
- val databaseConfig = DatabaseConfig(
- url = databaseUrl,
- user = databaseUser,
- password = databasePassword,
- )
-
- val mailingConfig = if (arguments.isPresent(ArgumentsConstants.SMTP_HOST) ||
- !System.getenv(EnvironmentConstants.SMTP_HOST).isNullOrEmpty()) {
- MailerConfiguration.SMTP(
- host = arguments.getNamedOrNull(ArgumentsConstants.SMTP_HOST)
- ?: System.getenv(EnvironmentConstants.SMTP_HOST)
- ?: error(FailureMessages.MISSING_SMTP_HOST),
-
- port = arguments.getNamedIntOrNull(ArgumentsConstants.SMTP_PORT)
- ?: System.getenv(EnvironmentConstants.SMTP_PORT)?.toInt()
- ?: error(FailureMessages.MISSING_SMTP_PORT),
-
- user = arguments.getNamedOrNull(ArgumentsConstants.SMTP_USER)
- ?: System.getenv(EnvironmentConstants.SMTP_USER)
- ?: error(FailureMessages.MISSING_SMTP_USER),
-
- password = arguments.getNamedOrNull(ArgumentsConstants.SMTP_USER_PASSWORD)
- ?: System.getenv(EnvironmentConstants.SMTP_USER_PASSWORD),
-
- sender = arguments.getNamedOrNull(ArgumentsConstants.SMTP_SENDER_ADDRESS)
- ?: System.getenv(EnvironmentConstants.SMTP_SENDER_ADDRESS)
- ?: error(FailureMessages.MISSING_SMTP_SENDER),
- )
- } else if (!System.getenv(EnvironmentConstants.MAILERSEND_API_KEY).isNullOrEmpty()
- || arguments.isPresent(ArgumentsConstants.MAILERSEND_API_KEY)) {
- MailerConfiguration.MailerSend(
- configuration = MailerSendEmailsRepository.Configuration(
- apiKey = arguments.getNamedOrNull(ArgumentsConstants.MAILERSEND_API_KEY)
- ?: System.getenv(EnvironmentConstants.MAILERSEND_API_KEY)
- ?: error(FailureMessages.MISSING_MAILERSEND_API_KEY),
-
- sender = arguments.getNamedOrNull(ArgumentsConstants.MAILERSEND_SENDER)
- ?: System.getenv(EnvironmentConstants.MAILERSEND_SENDER)
- ?: error(FailureMessages.MISSING_MAILERSEND_SENDER),
-
- confirmationTemplateId = arguments.getNamedOrNull(ArgumentsConstants.MAILERSEND_CONFIRMATION_TEMPLATE)
- ?: System.getenv(EnvironmentConstants.MAILERSEND_CONFIRMATION_TEMPLATE)
- ?: error(FailureMessages.MISSING_MAILERSEND_CONFIRMATION_TEMPLATE),
-
- supportEmail = arguments.getNamedOrNull(ArgumentsConstants.MAILERSEND_SUPPORT_EMAIL)
- ?: System.getenv(EnvironmentConstants.MAILERSEND_SUPPORT_EMAIL)
- ?: error(FailureMessages.MISSING_MAILERSEND_SUPPORT_EMAIL),
- )
- )
- } else {
- error(FailureMessages.MISSING_MAILER)
- }
-
- val dynamicModule = module {
- single { databaseConfig }
- single { mailingConfig }
- }
-
- val koin = startKoin {
- modules(AppModule + dynamicModule)
- }.koin
-
- val rSocketServerJob = launch {
- startRSocketApi(
- port = rSocketPort,
- authorizationService = koin.get(),
- usersService = koin.get(),
- timersService = koin.get(),
- timerSessionsService = koin.get(),
- authInterceptor = koin.get(),
- )
- }
- rSocketServerJob.join()
-}
diff --git a/app/src/main/kotlin/io/timemates/backend/application/constants/ArgumentsConstants.kt b/app/src/main/kotlin/io/timemates/backend/application/constants/ArgumentsConstants.kt
deleted file mode 100644
index 1708f4c9..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/constants/ArgumentsConstants.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-package io.timemates.backend.application.constants
-
-internal object ArgumentsConstants {
- const val GRPC_PORT = "grpcPort"
- const val RSOCKET_PORT = "rSocketPort"
- const val DATABASE_URL = "databaseUrl"
- const val DATABASE_USER = "databaseUser"
- const val DATABASE_USER_PASSWORD = "databaseUserPassword"
- const val SMTP = "smtp"
- const val SMTP_HOST = "smtpHost"
- const val SMTP_PORT = "smtpPort"
- const val SMTP_USER = "smtpUser"
- const val SMTP_USER_PASSWORD = "smtpUserPassword"
- const val SMTP_SENDER_ADDRESS = "smtpSenderAddr"
- const val MAILERSEND = "mailersend"
- const val MAILERSEND_API_KEY = "mailersendApiKey"
- const val MAILERSEND_SENDER = "mailersendSender"
- const val MAILERSEND_RECIPIENT = "mailersendRecipient"
- const val MAILERSEND_CONFIRMATION_TEMPLATE = "mailersendTemplatesConfirmation"
- const val MAILERSEND_SUPPORT_EMAIL = "mailersendSupportEmail"
- const val FILES_PATH = "filesPath"
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/constants/EnvironmentConstants.kt b/app/src/main/kotlin/io/timemates/backend/application/constants/EnvironmentConstants.kt
deleted file mode 100644
index 9f872a06..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/constants/EnvironmentConstants.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package io.timemates.backend.application.constants
-
-internal object EnvironmentConstants {
- private const val TIME_MATES_PREFIX = "TIMEMATES"
- private const val MAILERSEND_PREFIX = "MAILERSEND"
-
- const val GRPC_PORT = "${TIME_MATES_PREFIX}_GRPC_PORT"
- const val RSOCKET_PORT = "${TIME_MATES_PREFIX}_RSOCKET_PORT"
-
- // Database
- const val DATABASE_URL = "${TIME_MATES_PREFIX}_DATABASE_URL"
- const val DATABASE_USER = "${TIME_MATES_PREFIX}_DATABASE_USER"
- const val DATABASE_USER_PASSWORD = "${TIME_MATES_PREFIX}_DATABASE_USER_PASSWORD"
-
- // SMTP
- const val SMTP_HOST = "${TIME_MATES_PREFIX}_SMTP_HOST"
- const val SMTP_PORT = "${TIME_MATES_PREFIX}_SMTP_PORT"
- const val SMTP_USER = "${TIME_MATES_PREFIX}_SMTP_USER"
- const val SMTP_USER_PASSWORD = "${TIME_MATES_PREFIX}_SMTP_USER_PASSWORD"
- const val SMTP_SENDER_ADDRESS = "${TIME_MATES_PREFIX}_SMTP_SENDER_ADDRESS"
-
- // MailerSend
- const val MAILERSEND_API_KEY = "${MAILERSEND_PREFIX}_API_KEY"
- const val MAILERSEND_SENDER = "${MAILERSEND_PREFIX}_SENDER"
- const val MAILERSEND_CONFIRMATION_TEMPLATE = "${MAILERSEND_PREFIX}_CONFIRMATION_TEMPLATE"
- const val MAILERSEND_SUPPORT_EMAIL = "${MAILERSEND_PREFIX}_SUPPORT_EMAIL"
-
- // Other constants
- const val FILES_PATH = "${TIME_MATES_PREFIX}_FILES_PATH"
-}
diff --git a/app/src/main/kotlin/io/timemates/backend/application/constants/FailureMessages.kt b/app/src/main/kotlin/io/timemates/backend/application/constants/FailureMessages.kt
deleted file mode 100644
index 0ce23c1a..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/constants/FailureMessages.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package io.timemates.backend.application.constants
-
-internal object FailureMessages {
- const val MISSING_PORT = "Please provide a valid port number."
- const val MISSING_DATABASE_URL = "Please provide a database URL."
- const val MISSING_SMTP_HOST = "You're missing the SMTP host."
- const val MISSING_SMTP_PORT = "You're missing the SMTP port."
- const val MISSING_SMTP_USER = "You're missing the SMTP user."
- const val MISSING_SMTP_SENDER = "You're missing the SMTP sender."
- const val MISSING_MAILERSEND_API_KEY = "You're missing the API key for MailerSend."
- const val MISSING_MAILERSEND_SUPPORT_EMAIL = "You're missing the support email for MailerSend."
- const val MISSING_MAILERSEND_SENDER = "You're missing the author of the email (MailerSend sender)."
- const val MISSING_MAILERSEND_CONFIRMATION_TEMPLATE = "You're missing the template for confirmations for MailerSend."
- const val MISSING_MAILER = "You have not specified a mailer, please refer to documentation for correct setup."
- const val MISSING_FILES_PATH = "You're missing the files path."
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/AppModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/AppModule.kt
deleted file mode 100644
index c7c8e739..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/AppModule.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-val AppModule = listOf(
- DatabaseModule,
- CommonModule,
- UsersModule,
- TimersModule,
- TimerSessionsModule,
- FilesModule,
- AuthorizationsModule,
- RSocketServicesModule,
-)
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/AuthorizationsModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/AuthorizationsModule.kt
deleted file mode 100644
index f8c90575..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/AuthorizationsModule.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.repositories.VerificationsRepository
-import io.timemates.backend.authorization.usecases.*
-import io.timemates.backend.data.authorization.PostgresqlAuthorizationsRepository
-import io.timemates.backend.data.authorization.PostgresqlVerificationsRepository
-import io.timemates.backend.data.authorization.cache.CacheAuthorizationsDataSource
-import io.timemates.backend.data.authorization.db.TableAuthorizationsDataSource
-import io.timemates.backend.data.authorization.db.TableVerificationsDataSource
-import io.timemates.backend.data.authorization.db.mapper.DbAuthorizationsMapper
-import io.timemates.backend.data.authorization.db.mapper.DbVerificationsMapper
-import io.timemates.backend.data.authorization.mapper.AuthorizationsMapper
-import io.timemates.backend.data.authorization.mapper.VerificationsMapper
-import org.koin.core.module.dsl.singleOf
-import org.koin.dsl.module
-import kotlin.time.Duration.Companion.minutes
-
-val AuthorizationsModule = module {
- singleOf(::TableAuthorizationsDataSource)
- single {
- CacheAuthorizationsDataSource(1000, 5.minutes)
- }
- singleOf(::DbAuthorizationsMapper)
- single {
- PostgresqlVerificationsRepository(TableVerificationsDataSource(get(), DbVerificationsMapper()), VerificationsMapper())
- }
- single {
- PostgresqlAuthorizationsRepository(get(), get(), get())
- }
- singleOf(::AuthorizationsMapper)
- singleOf(::GetAuthorizationUseCase)
- singleOf(::GetAuthorizationsUseCase)
-
- // Use cases
- singleOf(::AuthByEmailUseCase)
- singleOf(::ConfigureNewAccountUseCase)
- singleOf(::RefreshTokenUseCase)
- singleOf(::RemoveAccessTokenUseCase)
- singleOf(::VerifyAuthorizationUseCase)
- singleOf(::GetAuthorizationUseCase)
- singleOf(::GetUserIdByAccessTokenUseCase)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/CommonModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/CommonModule.kt
deleted file mode 100644
index f60c08da..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/CommonModule.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-import com.timemates.backend.time.SystemTimeProvider
-import com.timemates.backend.time.TimeProvider
-import com.timemates.random.RandomProvider
-import com.timemates.random.SecureRandomProvider
-import io.timemates.backend.application.dependencies.configuration.MailerConfiguration
-import io.timemates.backend.common.repositories.EmailsRepository
-import io.timemates.backend.data.common.repositories.MailerSendEmailsRepository
-import io.timemates.backend.data.common.repositories.SMTPEmailsRepository
-import io.timemates.backend.mailer.SMTPMailer
-import kotlinx.serialization.json.Json
-import org.koin.dsl.module
-import timemates.backend.hashing.HashingRepository
-import java.time.ZoneId
-import timemates.backend.hashing.repository.HashingRepository as HashingRepositoryContract
-
-val CommonModule = module {
- single {
- SystemTimeProvider(ZoneId.of("UTC"))
- }
- single {
- SecureRandomProvider()
- }
- single {
-
- when (val configuration = get()) {
- is MailerConfiguration.MailerSend -> {
- MailerSendEmailsRepository(configuration.configuration)
- }
-
- is MailerConfiguration.SMTP -> {
- val mailer = SMTPMailer(
- host = configuration.host,
- user = configuration.user,
- port = configuration.port,
- password = configuration.password,
- sender = configuration.sender,
- )
-
- SMTPEmailsRepository(mailer)
- }
- }
- }
-
- single {
- Json {
- encodeDefaults = false
- ignoreUnknownKeys = true
- prettyPrint = false
- }
- }
- single {
- HashingRepository()
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/DatabaseModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/DatabaseModule.kt
deleted file mode 100644
index aa319c54..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/DatabaseModule.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-import io.timemates.backend.application.dependencies.configuration.DatabaseConfig
-import org.jetbrains.exposed.sql.Database
-import org.koin.dsl.module
-
-val DatabaseModule = module {
- single {
- val config = get()
-
- Database.connect(
- url = config.url,
- user = config.user,
- password = config.password ?: "",
- )
- }
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/FilesModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/FilesModule.kt
deleted file mode 100644
index f140ed23..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/FilesModule.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-import io.timemates.backend.data.files.datasource.FileEntityMapper
-import io.timemates.backend.data.files.datasource.LocalFilesDataSource
-import io.timemates.backend.data.files.datasource.PostgresqlFilesDataSource
-import io.timemates.backend.files.repositories.FilesRepository
-import io.timemates.backend.files.usecases.GetImageUseCase
-import io.timemates.backend.files.usecases.UploadFileUseCase
-import org.koin.core.module.dsl.singleOf
-import org.koin.core.qualifier.named
-import org.koin.dsl.module
-import java.net.URI
-import java.nio.file.Path
-import io.timemates.backend.data.files.FilesRepository as LocalFilesRepository
-
-val filesPathName = named("files.path")
-
-val FilesModule = module {
- single {
- LocalFilesDataSource(Path.of(get(filesPathName)))
- }
- singleOf(::PostgresqlFilesDataSource)
- singleOf(::FileEntityMapper)
- single {
- LocalFilesRepository(get(), get(), get())
- }
-
- // Use cases
- singleOf(::GetImageUseCase)
- singleOf(::UploadFileUseCase)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/RSocketServicesModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/RSocketServicesModule.kt
deleted file mode 100644
index e27fc965..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/RSocketServicesModule.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-import io.timemates.api.rsocket.auth.AuthInterceptor
-import io.timemates.api.rsocket.auth.AuthorizationService
-import io.timemates.api.rsocket.timers.TimersService
-import io.timemates.api.rsocket.timers.sessions.TimerSessionsService
-import io.timemates.api.rsocket.users.UsersService
-import org.koin.core.module.dsl.singleOf
-import org.koin.dsl.module
-
-val RSocketServicesModule = module {
- singleOf(::UsersService)
- singleOf(::AuthorizationService)
- singleOf(::TimersService)
- singleOf(::TimerSessionsService)
- singleOf(::AuthInterceptor)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/TimerSessionsModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/TimerSessionsModule.kt
deleted file mode 100644
index c979f556..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/TimerSessionsModule.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-import io.timemates.backend.data.timers.CoPostgresqlTimerSessionRepository
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.usecases.sessions.*
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import org.koin.core.module.dsl.singleOf
-import org.koin.dsl.module
-
-val TimerSessionsModule = module {
- single {
- CoPostgresqlTimerSessionRepository(
- coroutineScope = CoroutineScope(Dispatchers.Default.limitedParallelism(10)),
- tableTimersSessionUsers = get(),
- tableTimersStateDataSource = get(),
- timeProvider = get(),
- timersRepository = get(),
- sessionsMapper = get(),
- )
- }
-
- singleOf(::JoinSessionUseCase)
- singleOf(::LeaveSessionUseCase)
- singleOf(::PingSessionUseCase)
- singleOf(::ConfirmStartUseCase)
- singleOf(::GetStateUpdatesUseCase)
- singleOf(::GetCurrentTimerSessionUseCase)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/TimersModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/TimersModule.kt
deleted file mode 100644
index a5cac56d..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/TimersModule.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-import io.timemates.backend.data.timers.PostgresqlTimerInvitesRepository
-import io.timemates.backend.data.timers.PostgresqlTimersRepository
-import io.timemates.backend.data.timers.cache.CacheTimersDataSource
-import io.timemates.backend.data.timers.db.*
-import io.timemates.backend.data.timers.mappers.TimerInvitesMapper
-import io.timemates.backend.data.timers.mappers.TimerSessionMapper
-import io.timemates.backend.data.timers.mappers.TimersMapper
-import io.timemates.backend.timers.repositories.TimerInvitesRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.usecases.*
-import io.timemates.backend.timers.usecases.members.GetMembersUseCase
-import io.timemates.backend.timers.usecases.members.KickTimerUserUseCase
-import io.timemates.backend.timers.usecases.members.invites.CreateInviteUseCase
-import io.timemates.backend.timers.usecases.members.invites.GetInvitesUseCase
-import io.timemates.backend.timers.usecases.members.invites.JoinByInviteUseCase
-import io.timemates.backend.timers.usecases.members.invites.RemoveInviteUseCase
-import org.koin.core.module.dsl.singleOf
-import org.koin.dsl.module
-
-val TimersModule = module {
- singleOf(::TableTimersDataSource)
- singleOf(::TableTimersStateDataSource)
- singleOf(::TimerSessionMapper)
- singleOf(::TableTimersSessionUsersDataSource)
- single {
- CacheTimersDataSource(100)
- }
- singleOf(::TableTimerParticipantsDataSource)
- singleOf(::PostgresqlTimersRepository)
- single {
- PostgresqlTimersRepository(get(), get(), get(), get())
- }
- singleOf(::JoinByInviteUseCase)
- singleOf(::TimerSessionMapper)
- singleOf(::TimersMapper)
- singleOf(::GetTimersUseCase)
- singleOf(::SetTimerInfoUseCase)
- singleOf(::RemoveTimerUseCase)
- singleOf(::SetTimerSettingsUseCase)
- singleOf(::GetTimerUseCase)
- singleOf(::TableTimerInvitesDataSource)
- singleOf(::TimerInvitesMapper)
- single {
- PostgresqlTimerInvitesRepository(get(), get(), get())
- }
- singleOf(::GetInvitesUseCase)
- singleOf(::CreateInviteUseCase)
- singleOf(::RemoveInviteUseCase)
-
- singleOf(::TableTimerInvitesDataSource)
- singleOf(::TimerInvitesMapper)
- singleOf(::GetInvitesUseCase)
- singleOf(::CreateInviteUseCase)
- singleOf(::RemoveInviteUseCase)
- singleOf(::CreateInviteUseCase)
- singleOf(::CreateTimerUseCase)
- singleOf(::RemoveTimerUseCase)
- singleOf(::SetTimerInfoUseCase)
- singleOf(::GetInvitesUseCase)
- singleOf(::GetMembersUseCase)
- singleOf(::GetTimersUseCase)
- singleOf(::SetTimerSettingsUseCase)
- singleOf(::KickTimerUserUseCase)
- singleOf(::RemoveInviteUseCase)
- singleOf(::StartTimerUseCase)
- singleOf(::StopTimerUseCase)
- singleOf(::GetTimerUseCase)
-}
-
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/UsersModule.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/UsersModule.kt
deleted file mode 100644
index 552feed4..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/UsersModule.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package io.timemates.backend.application.dependencies
-
-import io.timemates.backend.data.users.PostgresqlUsersRepository
-import io.timemates.backend.data.users.UserEntitiesMapper
-import io.timemates.backend.data.users.datasource.CachedUsersDataSource
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource
-import io.timemates.backend.users.repositories.UsersRepository
-import io.timemates.backend.users.usecases.EditUserUseCase
-import io.timemates.backend.users.usecases.GetUsersUseCase
-import org.koin.core.module.dsl.singleOf
-import org.koin.dsl.module
-
-val UsersModule = module {
- singleOf(::PostgresqlUsersDataSource)
- single {
- CachedUsersDataSource(100)
- }
- singleOf(::UserEntitiesMapper)
- single {
- PostgresqlUsersRepository(get(), get(), get(), get())
- }
-
- // Use cases
- singleOf(::EditUserUseCase)
- singleOf(::GetUsersUseCase)
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/configuration/DatabaseConfig.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/configuration/DatabaseConfig.kt
deleted file mode 100644
index 7d363287..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/configuration/DatabaseConfig.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package io.timemates.backend.application.dependencies.configuration
-
-data class DatabaseConfig(
- val url: String,
- val user: String,
- val password: String?,
-)
\ No newline at end of file
diff --git a/app/src/main/kotlin/io/timemates/backend/application/dependencies/configuration/MailerConfig.kt b/app/src/main/kotlin/io/timemates/backend/application/dependencies/configuration/MailerConfig.kt
deleted file mode 100644
index 2fb87d40..00000000
--- a/app/src/main/kotlin/io/timemates/backend/application/dependencies/configuration/MailerConfig.kt
+++ /dev/null
@@ -1,17 +0,0 @@
-package io.timemates.backend.application.dependencies.configuration
-
-import io.timemates.backend.data.common.repositories.MailerSendEmailsRepository
-
-sealed class MailerConfiguration {
- data class SMTP(
- val host: String,
- val port: Int,
- val user: String,
- val password: String?,
- val sender: String,
- ) : MailerConfiguration()
-
- data class MailerSend(
- val configuration: MailerSendEmailsRepository.Configuration,
- ) : MailerConfiguration()
-}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/timemates/backend/application/Application.kt b/app/src/main/kotlin/org/timemates/backend/application/Application.kt
new file mode 100644
index 00000000..678f7191
--- /dev/null
+++ b/app/src/main/kotlin/org/timemates/backend/application/Application.kt
@@ -0,0 +1,97 @@
+@file:Suppress("ExtractKtorModule")
+
+package org.timemates.backend.application
+
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.runBlocking
+import org.timemates.api.rsocket.startRSocketApi
+import org.timemates.backend.cli.getNamedIntOrNull
+import org.timemates.backend.cli.parseArguments
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+/**
+ * Application entry-point.
+ *
+ * **Input Configuration**:
+ * This application can be configured using both environment variables and program arguments.
+ * The arguments take precedence, falling back to environment variables if not provided.
+ *
+ * **Environment Variables**:
+ * - `application_infrastructure_rsocket_port`: The RSocket port. Defaults to 8080 if not provided.
+ * - `application_database_url`: The database URL.
+ * - `application_database_user`: The database username.
+ * - `application_database_password`: The database password.
+ * - `mailersend_api_key`: MailerSend API key.
+ * - `mailersend_sender`: MailerSend sender.
+ * - `mailersend_templates_auth_confirmation`: MailerSend template for authentication confirmation.
+ * - `mailersend_support_email`: Support email for MailerSend.
+ * - `timers_cache_size`: Cache size for timers. Defaults to 100 if not provided.
+ * - `users_cache_size`: Cache size for users. Defaults to 100 if not provided.
+ * - `auth_cache_size`: Cache size for authentication entities. Defaults to 100 if not provided.
+ * - `auth_cache_alive`: Maximum alive time for authentication cache in seconds. Defaults to 5 minutes if not provided.
+ *
+ * **Program Arguments**:
+ * - `--debug`: Enable debug mode.
+ *
+ * **Defaults**:
+ * - RSocket Port: 8080
+ * - Timers Cache Size: 100
+ * - Users Cache Size: 100
+ * - Auth Cache Size: 100
+ * - Auth Cache Alive Time: 5 minutes
+ * - Debug Mode: Disabled by default.
+ *
+ * @param args An array of program arguments.
+ */
+fun main(args: Array): Unit = runBlocking {
+ val arguments = args.parseArguments()
+
+ val rSocketPort = arguments.getNamedIntOrNull("rsocketPort")
+ ?: getEnvOrThrow("application_infrastructure_rsocket_port").toIntOrNull()
+ ?: 8080
+
+ val databaseUrl = arguments.getNamedOrNull("databaseUrl")
+ ?: getEnvOrThrow("application_database_url")
+ val databaseUser = arguments.getNamedOrNull("databaseUser")
+ ?: getEnvOrThrow("application_database_user")
+ val databasePassword = arguments.getNamedOrNull("databasePassword")
+ ?: getEnvOrThrow("application_database_password")
+
+ val mailerSendApiKey = arguments.getNamedOrNull("mailersendApiKey")
+ ?: getEnvOrThrow("mailersend_api_key")
+ val mailerSendSender = arguments.getNamedOrNull("mailersendSender")
+ ?: getEnvOrThrow("mailersend_sender")
+ val mailerSendConfirmationTemplateId =
+ arguments.getNamedOrNull("mailersendTemplatesConfirmation")
+ ?: getEnvOrThrow("mailersend_templates_auth_confirmation")
+ val mailerSendSupportEmail = arguments.getNamedOrNull("mailersendSupportEmail")
+ ?: getEnvOrThrow("mailersend_support_email")
+
+ val koin = initDeps(
+ databaseUrl = databaseUrl,
+ databaseUser = databaseUser,
+ databasePassword = databasePassword,
+ mailerSendApiKey = mailerSendApiKey,
+ mailerSendSender = mailerSendSender,
+ mailerSendSupportEmail = mailerSendSupportEmail,
+ mailerSendConfirmationTemplateId = mailerSendConfirmationTemplateId,
+ timersCacheSize = System.getenv("timers_cache_size")?.toLongOrNull() ?: 100L,
+ usersCacheSize = System.getenv("users_cache_size")?.toLongOrNull() ?: 100L,
+ authMaxCacheEntities = System.getenv("auth_cache_size")?.toLongOrNull() ?: 100L,
+ authMaxAliveTime = System.getenv("auth_cache_alive")?.toLongOrNull()?.seconds ?: 5.minutes,
+ isDebug = arguments.isPresent("debug"),
+ )
+
+ val rSocketServerJob = launch {
+ startRSocketApi(
+ port = rSocketPort,
+ authorizationService = koin.get(),
+ usersService = koin.get(),
+ timersService = koin.get(),
+ timerSessionsService = koin.get(),
+ authInterceptor = koin.get(),
+ )
+ }
+ rSocketServerJob.join()
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/timemates/backend/application/Dependencies.kt b/app/src/main/kotlin/org/timemates/backend/application/Dependencies.kt
new file mode 100644
index 00000000..cb29a624
--- /dev/null
+++ b/app/src/main/kotlin/org/timemates/backend/application/Dependencies.kt
@@ -0,0 +1,80 @@
+package org.timemates.backend.application
+
+import com.timemates.backend.time.SystemTimeProvider
+import com.timemates.backend.time.TimeProvider
+import com.timemates.random.RandomProvider
+import com.timemates.random.SecureRandomProvider
+import org.jetbrains.exposed.sql.Database
+import org.koin.core.Koin
+import org.koin.core.context.startKoin
+import org.koin.core.qualifier.StringQualifier
+import org.koin.dsl.module
+import org.koin.ksp.generated.module
+import org.timemates.api.rsocket.RSProtoServicesModule
+import org.timemates.backend.auth.deps.AuthorizationsModule
+import org.timemates.backend.timers.deps.TimersModule
+import org.timemates.backend.users.deps.UsersModule
+import kotlin.time.Duration
+
+fun initDeps(
+ databaseUser: String,
+ databasePassword: String,
+ databaseUrl: String,
+ mailerSendApiKey: String,
+ mailerSendSender: String,
+ mailerSendConfirmationTemplateId: String,
+ mailerSendSupportEmail: String,
+ timersCacheSize: Long,
+ usersCacheSize: Long,
+ authMaxCacheEntities: Long,
+ authMaxAliveTime: Duration,
+ isDebug: Boolean,
+): Koin {
+ return startKoin {
+ val databaseModule = module {
+ single {
+ return@single Database.connect(
+ url = databaseUrl,
+ user = databaseUser,
+ password = databasePassword,
+ )
+ }
+ }
+
+ val mailersendModule = module {
+ single(StringQualifier("mailersend.apiKey")) { mailerSendApiKey }
+ single(StringQualifier("mailersend.supportEmail")) { mailerSendSupportEmail }
+ single(StringQualifier("mailersend.sender")) { mailerSendSender }
+ single(StringQualifier("mailersend.apiKey")) { mailerSendApiKey }
+ single(StringQualifier("mailersend.templates.confirmation")) { mailerSendConfirmationTemplateId }
+ }
+
+ val applicationModule = module {
+ single(StringQualifier("application.isDebug")) { isDebug }
+ }
+
+ val cacheModule = module {
+ single(StringQualifier("timers.cache.size")) { timersCacheSize }
+ single(StringQualifier("users.cache.size")) { usersCacheSize }
+ single(StringQualifier("auth.cache.size")) { authMaxCacheEntities }
+ single(StringQualifier("auth.cache.alive")) { authMaxAliveTime }
+ }
+
+ val foundationModule = module {
+ single { SystemTimeProvider() }
+ single { SecureRandomProvider() }
+ }
+
+ modules(
+ databaseModule,
+ mailersendModule,
+ applicationModule,
+ cacheModule,
+ foundationModule,
+ UsersModule().module,
+ AuthorizationsModule().module,
+ TimersModule().module,
+ RSProtoServicesModule().module,
+ )
+ }.koin
+}
\ No newline at end of file
diff --git a/app/src/main/kotlin/org/timemates/backend/application/SystemUtils.kt b/app/src/main/kotlin/org/timemates/backend/application/SystemUtils.kt
new file mode 100644
index 00000000..7e8a7c48
--- /dev/null
+++ b/app/src/main/kotlin/org/timemates/backend/application/SystemUtils.kt
@@ -0,0 +1,6 @@
+package org.timemates.backend.application
+
+internal fun getEnvOrThrow(name: String): String {
+ return System.getenv(name)
+ ?: error("Environment variable '$name' is not defined in the running scope.")
+}
\ No newline at end of file
diff --git a/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/Authorized.kt b/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/Authorized.kt
deleted file mode 100644
index 69eb49b7..00000000
--- a/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/Authorized.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package io.timemates.backend.features.authorization
-
-import io.timemates.backend.features.authorization.types.AuthorizedId
-
-public class Authorized(
- public val authorizedId: AuthorizedId,
- public val scopes: List,
-)
\ No newline at end of file
diff --git a/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/AuthorizedContext.kt b/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/AuthorizedContext.kt
deleted file mode 100644
index a16dc1ff..00000000
--- a/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/AuthorizedContext.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.timemates.backend.features.authorization
-
-public interface AuthorizedContext {
- /**
- * Current user authorization. Stores user's id.
- */
- public val authorization: Authorized
-}
-
-/**
- * Creates [AuthorizedContext] with given scope.
- *
- * @param provider authorization provider of given scope. If
- * authorization does not have given [S]cope & authorization
- * is not valid, then it should return null.
- * @param onFailure invokes when [provider] returns null
- * @param block that invokes on success with provided user id.
- */
-public inline fun authorizationProvider(
- provider: () -> Authorized?,
- onFailure: () -> Nothing,
- block: context(AuthorizedContext) () -> R,
-): R {
- val authorized = provider() ?: onFailure()
-
- val scope = object : AuthorizedContext {
- override val authorization: Authorized = authorized
- }
-
- return with(scope) {
- block(this)
- }
-}
-
diff --git a/common/hashing/src/main/kotlin/timemates/backend/hashing/repository/HashingRepository.kt b/common/hashing/src/main/kotlin/timemates/backend/hashing/repository/HashingRepository.kt
deleted file mode 100644
index 73a79bd8..00000000
--- a/common/hashing/src/main/kotlin/timemates/backend/hashing/repository/HashingRepository.kt
+++ /dev/null
@@ -1,6 +0,0 @@
-package timemates.backend.hashing.repository
-
-interface HashingRepository {
-
- fun generateMD5Hash(value: String): String
-}
\ No newline at end of file
diff --git a/common/state-machine/build.gradle.kts b/common/state-machine/build.gradle.kts
deleted file mode 100644
index 874bfce7..00000000
--- a/common/state-machine/build.gradle.kts
+++ /dev/null
@@ -1,13 +0,0 @@
-plugins {
- id(libs.plugins.jvm.module.convention.get().pluginId)
-}
-
-dependencies {
- implementation(projects.common.time)
- implementation(projects.common.validation)
- implementation(libs.kotlinx.coroutines)
-}
-
-kotlin {
- explicitApi()
-}
\ No newline at end of file
diff --git a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/CoroutinesStateMachine.kt b/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/CoroutinesStateMachine.kt
deleted file mode 100644
index 2a53a58f..00000000
--- a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/CoroutinesStateMachine.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-package io.timemates.backend.fsm
-
-import com.timemates.backend.time.TimeProvider
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.sync.Mutex
-import kotlinx.coroutines.sync.withLock
-import java.util.concurrent.ConcurrentHashMap
-
-public fun > CoroutinesStateMachine(
- coroutineScope: CoroutineScope,
- timeProvider: TimeProvider,
- storage: StateStorage? = null,
-): CoroutinesStateMachine = with(coroutineScope) {
- CoroutinesStateMachine(storage, timeProvider)
-}
-
-/**
- * Implementation of [StateMachine] using [kotlinx.coroutines] with ability to save states
- * to providable [storage]. By default, it's only saving to memory.
- *
- * @param KeyType the type of the keys that identify each state machine.
- * @param EventType the type of the events that are processed by the state machine.
- */
-context (CoroutineScope)
-public class CoroutinesStateMachine>(
- private val storage: StateStorage? = null,
- private val timeProvider: TimeProvider,
-) : StateMachine {
-
- /**
- * A concurrent hash map that holds the current state of each state machine identified by a [KeyType].
- */
- private val states: ConcurrentHashMap> = ConcurrentHashMap()
-
- /**
- * A concurrent hash map that holds the shared flow of events for each state machine identified by a [KeyType].
- */
- private val events: ConcurrentHashMap> = ConcurrentHashMap()
-
- /**
- * A concurrent hash map that holds the [Mutex] for each state machine identified by a [KeyType].
- */
- private val mutexes = ConcurrentHashMap()
-
- /**
- * Sets the state of the state machine identified by a [KeyType].
- *
- * @param key the key that identifies the state machine.
- * @param state the new state to be set.
- */
- override suspend fun setState(key: KeyType, state: StateType) {
- // if state is the same, we should ignore upcoming state
- if (states[key] == state)
- return
-
- // check whether other state is present, if so, we cancel and remove it from
- // queue and set current state
- if (states[key] == null) {
- createStateAsync(key, state, true)
- } else {
- // we do not remove state on-purpose, as we will
- // reuse object on next iteration
- /* states.remove(key) */
- events.remove(key)
- mutexes.remove(key)
-
- setState(key, state)
- }
- }
-
- @Suppress("UNCHECKED_CAST")
- private fun createStateAsync(key: KeyType, state: StateType, saveAtFirst: Boolean) {
- val mutex = mutexes.computeIfAbsent(key) { Mutex() }
- launch {
- mutex.withLock {
- // If the state machine does not exist, create it
- val transformedState = state.onEnter()
- // probably, there was different state before, so we keep subscribers
- // notified and reuse state flow
- val flow = states[key] ?: MutableStateFlow(transformedState as StateType)
-
- if (saveAtFirst)
- storage?.save(key, state)
-
- // Store the CoroutineScope, state flow, and event flow in
- // the respective hash maps.
- states[key] = flow
- events[key] = MutableSharedFlow()
-
- // Launch a coroutine to collect events and send it to current state.
- launch {
- events[key]?.collectLatest { event ->
- transformedState.onEvent(event)
- .takeIf { it != flow.value }
- ?.let { it as StateType }
- ?.also {
- flow.value = it
- storage?.save(key, state)
- }
- }
- }
-
- with(flow) {
- // Launch a coroutine to handle the state's timeout.
- launch {
- delay(timeProvider.provide() - transformedState.publishTime + transformedState.alive)
- transformedState.onTimeout()?.let { emit(it as StateType) } ?: run {
- // If the state machine has timed out and there's no
- // pending states, remove it from the hash maps.
- cancel()
- states.remove(key)
- events.remove(key)
- mutexes.remove(key)
- }
- }
- }
- }
- }
- }
-
- /**
- * Sends an event to the state machine identified by a [KeyType].
- *
- * @param key the key that identifies the state machine.
- * @param event the event to be sent.
- */
- override suspend fun sendEvent(key: KeyType, event: EventType): Boolean {
- events[key]?.emit(event) ?: return false
-
- return true
- }
-
- /**
- * Gets the state flow of the state machine identified by a [KeyType].
- *
- * @param key the key that identifies the state machine.
- * @return a [Flow] that emits the current state of the state machine.
- */
- override suspend fun getState(key: KeyType): Flow? {
- states[key]?.let { state -> return state }
- storage?.load(key)?.let { state -> createStateAsync(key, state, false) }
-
- return states[key]
- }
-}
\ No newline at end of file
diff --git a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/State.kt b/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/State.kt
deleted file mode 100644
index f78101b9..00000000
--- a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/State.kt
+++ /dev/null
@@ -1,46 +0,0 @@
-package io.timemates.backend.fsm
-
-import com.timemates.backend.time.UnixTime
-import kotlin.time.Duration
-
-/**
- * An abstract class representing a state in a finite state machine.
- * @param Event the type of the events that this state can handle.
- *
- * **Should implement equals & hashCode**
- */
-public abstract class State {
-
- /**
- * The duration that this state should remain alive.
- */
- public abstract val alive: Duration
-
- /**
- * When state was present.
- */
- public abstract val publishTime: UnixTime
-
- /**
- * Handles an event and returns the next state.
- * @param event the event to handle.
- * @return the next state or current if no need to change.
- */
- public open suspend fun onEvent(event: Event): State = this
-
- /**
- * Called when entering this state, before any events are processed.
- * Can be used to perform any setup or initialization that is required
- * before processing events.
- * @return the current state by default or the next / transformed state.
- */
- public open suspend fun onEnter(): State = this
-
- /**
- * Called when the alive duration for this state has elapsed.
- * Can be used to transition to a new state if required.
- *
- * @return the next state, or null to finish [StateMachine].
- */
- public open suspend fun onTimeout(): State? = null
-}
\ No newline at end of file
diff --git a/common/test-utils/build.gradle.kts b/common/test-utils/build.gradle.kts
deleted file mode 100644
index c3c0308e..00000000
--- a/common/test-utils/build.gradle.kts
+++ /dev/null
@@ -1,11 +0,0 @@
-plugins {
- id(libs.plugins.jvm.module.convention.get().pluginId)
-}
-
-dependencies {
- implementation(projects.common.authorization)
- implementation(projects.common.validation)
- implementation(projects.core)
-
- implementation(libs.kotlin.test)
-}
\ No newline at end of file
diff --git a/common/test-utils/src/main/kotlin/io/timemates/backend/testing/auth/testAuthContext.kt b/common/test-utils/src/main/kotlin/io/timemates/backend/testing/auth/testAuthContext.kt
deleted file mode 100644
index 46b3901e..00000000
--- a/common/test-utils/src/main/kotlin/io/timemates/backend/testing/auth/testAuthContext.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.timemates.backend.testing.auth
-
-import io.timemates.backend.features.authorization.Authorized
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.features.authorization.Scope
-import io.timemates.backend.features.authorization.types.AuthorizedId
-import io.timemates.backend.testing.validation.createOrAssert
-import io.timemates.backend.users.types.value.UserId
-
-/**
- * Authorization scope for text purposes. Only usage of it is to provide [AuthorizedContext] of given scope [T].
- */
-inline fun testAuthContext(
- userId: UserId = UserId.createOrAssert(0),
- block: AuthorizedContext.() -> R,
-): R {
- val context = object : AuthorizedContext {
- override val authorization: Authorized = Authorized(
- authorizedId = AuthorizedId(userId.long),
- scopes = listOf(Scope.All)
- )
- }
-
- return context.let(block)
-}
\ No newline at end of file
diff --git a/common/test-utils/src/main/kotlin/io/timemates/backend/testing/validation/createOrAssert.kt b/common/test-utils/src/main/kotlin/io/timemates/backend/testing/validation/createOrAssert.kt
deleted file mode 100644
index 6645506b..00000000
--- a/common/test-utils/src/main/kotlin/io/timemates/backend/testing/validation/createOrAssert.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.timemates.backend.testing.validation
-
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import org.jetbrains.annotations.TestOnly
-
-val ValidationFailureHandler.Companion.THROWS_ASSERTION_ERROR by lazy {
- ValidationFailureHandler { failure ->
- throw AssertionError(failure.string)
- }
-}
-
-/**
- * A function for testing to pass / fail test where [ValidationFailureHandler]
- * is needed.
- *
- * @throws AssertionError if the validation fails.
- */
-@TestOnly
-@Throws(AssertionError::class)
-fun SafeConstructor.createOrAssert(
- w: W,
-): T = with(ValidationFailureHandler.THROWS_ASSERTION_ERROR) {
- create(w)
-}
diff --git a/common/validation/src/main/kotlin/io/timemates/backend/validation/FailureMessage.kt b/common/validation/src/main/kotlin/io/timemates/backend/validation/FailureMessage.kt
deleted file mode 100644
index e2a8924c..00000000
--- a/common/validation/src/main/kotlin/io/timemates/backend/validation/FailureMessage.kt
+++ /dev/null
@@ -1,90 +0,0 @@
-package io.timemates.backend.validation
-
-/**
- * Class that contains human-readable message of
- * validation failure.
- */
-@JvmInline
-public value class FailureMessage(public val string: String) {
- public companion object {
- /**
- * Creates a `FailureMessage` object with a size constraint failure message.
- *
- * @param size The size range constraint for the creation failure.
- * @return The `FailureMessage` object with the specified size constraint failure message.
- */
- context (SafeConstructor<*, *>)
- public fun ofSize(size: IntRange): FailureMessage {
- return FailureMessage(
- "Constraint failure: $displayName size must be in range of $size"
- )
- }
-
- /**
- * Creates a [FailureMessage] with a constraint failure message based on the provided size.
- * @param size The expected size that caused the constraint failure.
- * @return A [FailureMessage] object with the constraint failure message.
- */
- context (SafeConstructor<*, *>)
- public fun ofSize(size: Int): FailureMessage {
- return FailureMessage(
- "Constraint failure: $displayName size must be exactly $size"
- )
- }
-
- /**
- * Creates a `FailureMessage` object for a minimum size value failure.
- *
- * @param size The minimum value for the constraint.
- * @return The `FailureMessage` object representing the constraint failure.
- */
- context (SafeConstructor<*, *>)
- public fun ofMin(size: Int): FailureMessage {
- return FailureMessage(
- "Constraint failure: $displayName minimal value is the $size"
- )
- }
-
- /**
- * Returns a [FailureMessage] indicating that the constraint failure is that
- * value cannot be negative.
- *
- * @return a [FailureMessage] indicating the constraint failure
- */
- context (SafeConstructor<*, *>)
- public fun ofNegative(): FailureMessage {
- return FailureMessage("Constraint failure: $displayName cannot be negative")
- }
-
- /**
- * Creates a [FailureMessage] with a constraint failure message for a blank value.
- * @return A [FailureMessage] object with the constraint failure message.
- */
- context (SafeConstructor<*, *>)
- public fun ofBlank(): FailureMessage {
- return FailureMessage("Constraint failure: $displayName provided value is empty")
- }
-
- /**
- * Creates a `FailureMessage` object with a pattern constraint failure message.
- *
- * @param regex The regular expression pattern constraint for the creation failure.
- * @return The `FailureMessage` object with the specified pattern constraint failure message.
- */
- context (SafeConstructor<*, *>)
- public fun ofPattern(regex: Regex): FailureMessage {
- return FailureMessage(
- "Constraint failure: $displayName value should match $regex"
- )
- }
-
-
- }
-
- /**
- * Returns [string]
- */
- override fun toString(): String {
- return string
- }
-}
\ No newline at end of file
diff --git a/common/validation/src/main/kotlin/io/timemates/backend/validation/SafeConstructor.kt b/common/validation/src/main/kotlin/io/timemates/backend/validation/SafeConstructor.kt
deleted file mode 100644
index e06a23a7..00000000
--- a/common/validation/src/main/kotlin/io/timemates/backend/validation/SafeConstructor.kt
+++ /dev/null
@@ -1,60 +0,0 @@
-package io.timemates.backend.validation
-
-import io.timemates.backend.validation.exceptions.InternalValidationFailure
-import io.timemates.backend.validation.markers.InternalThrowAbility
-
-/**
- * Abstraction for factories that construct value objects.
- * Next pattern should be applied to the factories:
- * - Factory should be in companion object that does only one thing – constructing.
- * - Validation information (like sizes or patterns) should be on the top of
- * the factories in order to better readability.
- * - After validation information comes [create] and, if needed, constants
- * with messages below the method.
- *
- * This is abstract class on purpose: to support clearness and readability of
- * value objects.
- *
- * **You should always implement a constructor for value objects, even if
- * there is no actual restrictions on given type. It will help to minimize
- * possible changes and support existing code style rules.**
- */
-public abstract class SafeConstructor {
- /**
- * Name of the class what is validated. Used to display for API
- * responses.
- */
- public abstract val displayName: String
-
- /**
- * Method to construct valid instance of [Type].
- *
- * In addition, this function can transform input if needed (for example,
- * to remove multiple spaces or something like that, but it shouldn't
- * make something really different on user input to avoid misunderstanding
- * from user).
- *
- * @see ValidationFailureHandler
- * @return [Type] or fails in [ValidationFailureHandler].
- */
- context(ValidationFailureHandler)
- public abstract fun create(
- value: WrappedType,
- ): Type
-}
-
-/**
- * Constructs a [T] from [W] with validation check in unsafe way. You should
- * use it only if it comes from trusted source (like database or from generator)
- *
- * @see [ValidationFailureHandler]
- * @see [SafeConstructor.create]
- * @throws [com.timemates.backend.validation.exceptions.InternalValidationFailure] if validation failed.
- */
-context (InternalThrowAbility)
-@Throws(InternalValidationFailure::class)
-public fun SafeConstructor.createOrThrowInternally(value: W): T {
- return with(ValidationFailureHandler.THROWS_INTERNAL) {
- create(value)
- }
-}
diff --git a/common/validation/src/main/kotlin/io/timemates/backend/validation/ValidationFailureHandler.kt b/common/validation/src/main/kotlin/io/timemates/backend/validation/ValidationFailureHandler.kt
deleted file mode 100644
index 93cbc06d..00000000
--- a/common/validation/src/main/kotlin/io/timemates/backend/validation/ValidationFailureHandler.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package io.timemates.backend.validation
-
-import io.timemates.backend.validation.exceptions.InternalValidationFailure
-
-/**
- * Scope that handles validation failures and propagates
- * it to the top of the hierarchy.
- *
- * **Example:**
- * ```kotlin
- * context(ValidationFailureHandler)
- * fun editUserName(userName: String): Result {
- * // Finishes the execution on failure
- * val name: UserName = UserName.create(userName)
- *
- * val result = saveUserNameUseCase.execute(name)
- * // ...
- * }
- *
- * fun someRouting() = patch(..) {
- * withValidation({ return@patch call.respond(HttpStatus.BadRequest, it.string) }) {
- * editUserName(call.queryParameters["name])
- * }
- * }
- * ```
- */
-public fun interface ValidationFailureHandler {
- /**
- * Stops the execution and propagates failure to the top of the hierarchy.
- *
- * @param message – readable message for request output.
- */
- public fun onFail(message: FailureMessage): Nothing
-
- public companion object {
- /**
- * Scope that should be used if validation should always throw exception.
- *
- * @throws [InternalValidationFailure] if validation failed.
- */
- internal val THROWS_INTERNAL: ValidationFailureHandler = ValidationFailureHandler { failure ->
- throw InternalValidationFailure(failure.string)
- }
- }
-}
\ No newline at end of file
diff --git a/common/validation/src/main/kotlin/io/timemates/backend/validation/exceptions/InternalValidationFailure.kt b/common/validation/src/main/kotlin/io/timemates/backend/validation/exceptions/InternalValidationFailure.kt
deleted file mode 100644
index b4a61fd6..00000000
--- a/common/validation/src/main/kotlin/io/timemates/backend/validation/exceptions/InternalValidationFailure.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package io.timemates.backend.validation.exceptions
-
-internal class InternalValidationFailure(
- message: String,
-) : Exception("Validation failed with message: $message")
\ No newline at end of file
diff --git a/common/validation/src/main/kotlin/io/timemates/backend/validation/markers/InternalThrowAbility.kt b/common/validation/src/main/kotlin/io/timemates/backend/validation/markers/InternalThrowAbility.kt
deleted file mode 100644
index 1b3e6ad7..00000000
--- a/common/validation/src/main/kotlin/io/timemates/backend/validation/markers/InternalThrowAbility.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.timemates.backend.validation.markers
-
-/**
- * Interface-marker for functions that intended to be used only in context where's
- * safe to throw just exceptions, instead of handling it.
- *
- * Shouldn't be implemented directly to throw exceptions where it's needed, but
- * should create another interface-marker for that.
- *
- * @see [io.timemates.backend.common.markers.UseCase]
- */
-public interface InternalThrowAbility
\ No newline at end of file
diff --git a/common/validation/src/main/kotlin/io/timemates/backend/validation/reflection/wrapperTypeName.kt b/common/validation/src/main/kotlin/io/timemates/backend/validation/reflection/wrapperTypeName.kt
deleted file mode 100644
index 3e35ffcc..00000000
--- a/common/validation/src/main/kotlin/io/timemates/backend/validation/reflection/wrapperTypeName.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package io.timemates.backend.validation.reflection
-
-import io.timemates.backend.validation.SafeConstructor
-
-/**
- * Gets the name of wrapper for [SafeConstructor.displayName].
- */
-@Suppress("UnusedReceiverParameter")
-public inline fun SafeConstructor.wrapperTypeName(): Lazy =
- lazy { T::class.simpleName!! }
\ No newline at end of file
diff --git a/common/validation/src/test/kotlin/io/timemates/backend/validation/testValidationScope.kt b/common/validation/src/test/kotlin/io/timemates/backend/validation/testValidationScope.kt
deleted file mode 100644
index 9154df69..00000000
--- a/common/validation/src/test/kotlin/io/timemates/backend/validation/testValidationScope.kt
+++ /dev/null
@@ -1,9 +0,0 @@
-package io.timemates.backend.validation
-
-import io.timemates.backend.validation.ValidationFailureHandler
-
-fun testValidationScope(block: context(ValidationFailureHandler) () -> Unit) {
- val scope = ValidationFailureHandler { message ->
- throw AssertionError("Validation failed: $message.")
- }
-}
\ No newline at end of file
diff --git a/core/README.md b/core/README.md
deleted file mode 100644
index 4e7bdbef..00000000
--- a/core/README.md
+++ /dev/null
@@ -1,60 +0,0 @@
-# Core Module Documentation
-
-The `core` module is responsible for encapsulating the core domain logic of the application.
-
-## Purpose
-
-The purpose of the `core` module is to define and model the essential concepts of the application's domain. It contains the key components that represent the core domain entities, value objects, exceptions, use cases, and repositories.
-
-## Components
-
-### Types (entities)
-
-Entities represent the fundamental objects in the domain. They encapsulate behavior and hold state relevant to the business domain.
-
-### Value Objects
-
-Value objects are immutable objects that encapsulate a set of attributes or properties. They represent concepts within the domain and are characterized by their values rather than their identities.
-
-#### Validation Approach
-
-The `core` module follows an explicit validation approach without relying on exceptions. The goal is to ensure intentional validation and explicit handling of validation failures.
-
-##### SafeConstructor
-
-The `SafeConstructor` class is a key component for constructing valid instances of value objects with validation checks and explicit failure handling.
-
-- Factory Pattern: Each value object has a corresponding `SafeConstructor` implementation acting as a factory for creating valid instances.
-
-- Validation Information: `SafeConstructor` includes validation information for readability and understanding of the validation requirements.
-
-- Explicit Validation: The `create` method within `SafeConstructor` performs explicit validation, returning a valid instance or handling the failure explicitly.
-
-### Example: EmailAddress Value Object
-
-```kotlin
-value class EmailAddress private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String = "Email Address"
- private val SIZE = 3..200
- private val EMAIL_PATTERN = Regex("")
-
- override fun create(value: String): EmailAddress {
- return when {
- value.isEmpty() -> onFail(FailureMessage.ofBlank())
- value.length !in SIZE -> onFail(FailureMessage.ofSize(SIZE))
- !EMAIL_PATTERN.magtches(value) -> onFail(FailureMessage.ofPattern(EMAIL_PATTERN))
- else -> EmailAddress(value)
- }
- }
- }
-}
-```
-
-### Use Cases
-
-Use cases represent the application-specific business logic or operations that can be performed within the domain. They encapsulate the steps required to achieve a specific goal or fulfill a business requirement.
-
-### Repositories
-
-Repositories define contracts or interfaces for accessing and persisting domain entities. They provide an abstraction layer that decouples the domain logic from the underlying data storage or persistence mechanism.
diff --git a/core/build.gradle.kts b/core/build.gradle.kts
deleted file mode 100644
index dd478e6b..00000000
--- a/core/build.gradle.kts
+++ /dev/null
@@ -1,22 +0,0 @@
-plugins {
- id(libs.plugins.jvm.module.convention.get().pluginId)
-}
-
-dependencies {
- api(projects.common.authorization)
- api(projects.common.random)
- api(projects.common.validation)
- api(projects.common.pageToken)
- api(projects.common.time)
- api(projects.common.stateMachine)
-
- implementation(libs.kotlinx.coroutines)
- testImplementation(libs.kotlin.test)
- testImplementation(libs.junit.jupiter)
- testImplementation(libs.mockk)
- testImplementation(projects.common.testUtils)
-}
-
-tasks.withType {
- useJUnitPlatform()
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/Authorization.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/Authorization.kt
deleted file mode 100644
index c36119f1..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/Authorization.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package io.timemates.backend.authorization.types
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.features.authorization.Scope
-import io.timemates.backend.users.types.value.UserId
-
-data class Authorization(
- val userId: UserId,
- val accessHash: AccessHash,
- val refreshAccessHash: RefreshHash,
- val scopes: List,
- val expiresAt: UnixTime,
- val createdAt: UnixTime,
- val clientMetadata: ClientMetadata,
-)
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/Verification.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/Verification.kt
deleted file mode 100644
index bebc9b4b..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/Verification.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package io.timemates.backend.authorization.types
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.value.Attempts
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.users.types.value.EmailAddress
-
-data class Verification(
- val emailAddress: EmailAddress,
- val code: VerificationCode,
- val attempts: Attempts,
- val time: UnixTime,
- val isConfirmed: Boolean,
- val clientMetadata: ClientMetadata,
-)
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/ClientMetadata.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/ClientMetadata.kt
deleted file mode 100644
index d9768d97..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/ClientMetadata.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.timemates.backend.authorization.types.metadata
-
-import io.timemates.backend.authorization.types.metadata.value.ClientIpAddress
-import io.timemates.backend.authorization.types.metadata.value.ClientName
-import io.timemates.backend.authorization.types.metadata.value.ClientVersion
-
-data class ClientMetadata(
- val clientName: ClientName,
- val clientVersion: ClientVersion,
- val clientIpAddress: ClientIpAddress,
-)
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientIpAddress.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientIpAddress.kt
deleted file mode 100644
index 01db22d3..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientIpAddress.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.authorization.types.metadata.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class ClientIpAddress private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: String): ClientIpAddress {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- else -> ClientIpAddress(value)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientName.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientName.kt
deleted file mode 100644
index 9860dd12..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientName.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.authorization.types.metadata.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class ClientName private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: String): ClientName {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- else -> ClientName(value)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientVersion.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientVersion.kt
deleted file mode 100644
index 59411fea..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/metadata/value/ClientVersion.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.authorization.types.metadata.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class ClientVersion private constructor(val double: Double) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: Double): ClientVersion {
- return when {
- value < 1 -> onFail(FailureMessage.ofMin(1))
- else -> ClientVersion(value)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/AccessHash.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/value/AccessHash.kt
deleted file mode 100644
index bbaceee9..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/AccessHash.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package io.timemates.backend.authorization.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class AccessHash private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- const val SIZE = 128
-
- context(ValidationFailureHandler)
- override fun create(value: String): AccessHash {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- SIZE -> AccessHash(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(SIZE))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/Attempts.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/value/Attempts.kt
deleted file mode 100644
index b44a931d..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/Attempts.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.authorization.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class Attempts private constructor(val int: Int) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: Int): Attempts {
- return when {
- value < 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofNegative())
- else -> Attempts(value)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/AuthorizationId.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/value/AuthorizationId.kt
deleted file mode 100644
index 2014f466..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/AuthorizationId.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.authorization.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class AuthorizationId private constructor(val id: Int) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: Int): AuthorizationId {
- return when {
- value < 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofNegative())
- else -> AuthorizationId(value)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/RefreshHash.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/value/RefreshHash.kt
deleted file mode 100644
index 0e6a891c..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/RefreshHash.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.timemates.backend.authorization.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class RefreshHash private constructor(val string: String) {
- companion object : SafeConstructor() {
- const val SIZE = 128
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: String): RefreshHash {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- SIZE -> RefreshHash(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(SIZE))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/VerificationCode.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/value/VerificationCode.kt
deleted file mode 100644
index 03b4987c..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/VerificationCode.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.timemates.backend.authorization.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class VerificationCode private constructor(val string: String) {
- companion object : SafeConstructor() {
- const val SIZE = 8
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: String): VerificationCode {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- SIZE -> VerificationCode(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(SIZE))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/VerificationHash.kt b/core/src/main/kotlin/io/timemates/backend/authorization/types/value/VerificationHash.kt
deleted file mode 100644
index 9290fa59..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/value/VerificationHash.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.timemates.backend.authorization.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class VerificationHash private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- const val SIZE = 128
- context(ValidationFailureHandler)
- override fun create(value: String): VerificationHash {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- SIZE -> VerificationHash(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(SIZE))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/AuthByEmailUseCase.kt b/core/src/main/kotlin/io/timemates/backend/authorization/usecases/AuthByEmailUseCase.kt
deleted file mode 100644
index 65fbe581..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/AuthByEmailUseCase.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package io.timemates.backend.authorization.usecases
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.validation.createOrThrowInternally
-import com.timemates.random.RandomProvider
-import io.timemates.backend.authorization.repositories.VerificationsRepository
-import io.timemates.backend.authorization.types.Email
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.value.Attempts
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.authorization.types.value.VerificationHash
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.common.repositories.EmailsRepository
-import io.timemates.backend.users.types.value.EmailAddress
-import kotlin.time.Duration.Companion.hours
-import kotlin.time.Duration.Companion.minutes
-
-class AuthByEmailUseCase(
- private val emails: EmailsRepository,
- private val verifications: VerificationsRepository,
- private val timeProvider: TimeProvider,
- private val randomProvider: RandomProvider,
-) : UseCase {
- suspend fun execute(emailAddress: EmailAddress, clientMetadata: ClientMetadata): Result {
- // used for limits (max count of sessions & attempts that can be requested)
- val sessionsTimeBoundary = timeProvider.provide() - 1.hours
-
- return when {
- verifications.getNumberOfAttempts(emailAddress, sessionsTimeBoundary).int >= 9 ||
- verifications.getNumberOfSessions(emailAddress, sessionsTimeBoundary).int >= 3 ->
- Result.AttemptsExceed
-
- else -> {
- val code = VerificationCode.createOrThrowInternally(randomProvider.randomHash(VerificationCode.SIZE))
- val verificationHash = VerificationHash.createOrThrowInternally(randomProvider.randomHash(VerificationHash.SIZE))
- val expiresAt = timeProvider.provide() + 10.minutes
- val totalAttempts = Attempts.createOrThrowInternally(3)
-
- if (!emails.send(emailAddress, Email.AuthorizeEmail(emailAddress, code)))
- return Result.SendFailed
- verifications.save(emailAddress, verificationHash, code, expiresAt, totalAttempts, clientMetadata)
- Result.Success(verificationHash, timeProvider.provide() + 10.minutes, totalAttempts)
- }
- }
- }
-
- sealed interface Result {
- data object SendFailed : Result
- data class Success(
- val verificationHash: VerificationHash,
- val expiresAt: UnixTime,
- val attempts: Attempts,
- ) : Result
-
- data object AttemptsExceed : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/ConfigureNewAccountUseCase.kt b/core/src/main/kotlin/io/timemates/backend/authorization/usecases/ConfigureNewAccountUseCase.kt
deleted file mode 100644
index f649e203..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/ConfigureNewAccountUseCase.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package io.timemates.backend.authorization.usecases
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.validation.createOrThrowInternally
-import com.timemates.random.RandomProvider
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.repositories.VerificationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.authorization.types.value.VerificationHash
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.Scope
-import io.timemates.backend.users.repositories.UsersRepository
-import io.timemates.backend.users.types.value.UserDescription
-import io.timemates.backend.users.types.value.UserName
-import kotlin.time.Duration.Companion.days
-
-class ConfigureNewAccountUseCase(
- private val users: UsersRepository,
- private val authorizations: AuthorizationsRepository,
- private val verifications: VerificationsRepository,
- private val timeProvider: TimeProvider,
- private val randomProvider: RandomProvider,
-) : UseCase {
- suspend fun execute(
- verificationToken: VerificationHash,
- userName: UserName,
- shortBio: UserDescription?,
- ): Result {
- val verification = verifications.getVerification(verificationToken)
- ?.takeIf { it.isConfirmed }
- ?: return Result.NotFound
-
- val currentTime = timeProvider.provide()
-
- val accessHash = AccessHash.createOrThrowInternally(randomProvider.randomHash(AccessHash.SIZE))
- val refreshHash = RefreshHash.createOrThrowInternally(randomProvider.randomHash(RefreshHash.SIZE))
- val expiresAt = currentTime + 30.days
- val metadata = verification.clientMetadata
-
- val id = users.createUser(verification.emailAddress, userName, shortBio, timeProvider.provide())
- authorizations.create(
- id,
- accessHash,
- refreshHash,
- expiresAt,
- currentTime,
- metadata,
- )
-
- verifications.remove(verificationToken)
-
- return Result.Success(
- Authorization(
- userId = id,
- accessHash = accessHash,
- refreshAccessHash = refreshHash,
- scopes = listOf(Scope.All),
- expiresAt = expiresAt,
- createdAt = currentTime,
- clientMetadata = metadata,
- )
- )
- }
-
- sealed class Result {
- data object NotFound : Result()
- data class Success(val authorization: Authorization) : Result()
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetAuthorizationsUseCase.kt b/core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetAuthorizationsUseCase.kt
deleted file mode 100644
index 41f30dfa..00000000
--- a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetAuthorizationsUseCase.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package io.timemates.backend.authorization.usecases
-
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.AuthorizationsScope
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.users.types.value.userId
-
-class GetAuthorizationsUseCase(
- private val authorizationsRepository: AuthorizationsRepository,
-) : UseCase {
- context (AuthorizedContext)
- suspend fun execute(pageToken: PageToken?): Result {
- val result = authorizationsRepository.getList(userId, pageToken)
-
- return Result.Success(result.value, result.nextPageToken)
- }
-
- sealed class Result {
- data class Success(val list: List, val nextPageToken: PageToken?) : Result()
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/common/markers/Repository.kt b/core/src/main/kotlin/io/timemates/backend/common/markers/Repository.kt
deleted file mode 100644
index 9520d4b7..00000000
--- a/core/src/main/kotlin/io/timemates/backend/common/markers/Repository.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-package io.timemates.backend.common.markers
-
-import io.timemates.backend.validation.markers.InternalThrowAbility
-
-/**
- * Interface
- */
-interface Repository : InternalThrowAbility
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/common/markers/TypeDefaults.kt b/core/src/main/kotlin/io/timemates/backend/common/markers/TypeDefaults.kt
deleted file mode 100644
index 974a5885..00000000
--- a/core/src/main/kotlin/io/timemates/backend/common/markers/TypeDefaults.kt
+++ /dev/null
@@ -1,10 +0,0 @@
-package io.timemates.backend.common.markers
-
-import io.timemates.backend.validation.markers.InternalThrowAbility
-
-/**
- * Interface-marker for companions that have default variant of type (entity). This interface
- * marks that it's safe place to construct types in unsafe way or throw internal exceptions (as it's
- * not intended at all and should be handled properly)
- */
-interface TypeDefaults : InternalThrowAbility
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/common/markers/UseCase.kt b/core/src/main/kotlin/io/timemates/backend/common/markers/UseCase.kt
deleted file mode 100644
index a0ca3b87..00000000
--- a/core/src/main/kotlin/io/timemates/backend/common/markers/UseCase.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package io.timemates.backend.common.markers
-
-import io.timemates.backend.validation.markers.InternalThrowAbility
-
-interface UseCase : InternalThrowAbility
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/common/repositories/EmailsRepository.kt b/core/src/main/kotlin/io/timemates/backend/common/repositories/EmailsRepository.kt
deleted file mode 100644
index 69990945..00000000
--- a/core/src/main/kotlin/io/timemates/backend/common/repositories/EmailsRepository.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.timemates.backend.common.repositories
-
-import io.timemates.backend.authorization.types.Email
-import io.timemates.backend.common.markers.Repository
-import io.timemates.backend.users.types.value.EmailAddress
-
-interface EmailsRepository : Repository {
- /**
- * Sends email to [emailAddress] with [email].
- */
- suspend fun send(emailAddress: EmailAddress, email: Email): Boolean
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/common/types/value/Count.kt b/core/src/main/kotlin/io/timemates/backend/common/types/value/Count.kt
deleted file mode 100644
index a6153306..00000000
--- a/core/src/main/kotlin/io/timemates/backend/common/types/value/Count.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.common.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class Count private constructor(val int: Int) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: Int): Count {
- return when {
- value >= 0 -> Count(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofNegative())
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/common/types/value/Offset.kt b/core/src/main/kotlin/io/timemates/backend/common/types/value/Offset.kt
deleted file mode 100644
index a4d3a15a..00000000
--- a/core/src/main/kotlin/io/timemates/backend/common/types/value/Offset.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.common.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class Offset private constructor(val long: Long) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: Long): Offset {
- return when {
- value > 0 -> Offset(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofNegative())
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/files/repositories/FilesRepository.kt b/core/src/main/kotlin/io/timemates/backend/files/repositories/FilesRepository.kt
deleted file mode 100644
index c31d26c4..00000000
--- a/core/src/main/kotlin/io/timemates/backend/files/repositories/FilesRepository.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package io.timemates.backend.files.repositories
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.common.markers.Repository
-import io.timemates.backend.files.types.File
-import io.timemates.backend.files.types.FileType
-import io.timemates.backend.files.types.value.FileId
-import kotlinx.coroutines.flow.Flow
-import java.io.InputStream
-
-interface FilesRepository : Repository {
- suspend fun save(fileId: FileId, fileType: FileType, input: Flow, creationTime: UnixTime)
- suspend fun retrieve(file: File): InputStream?
- suspend fun remove(fileId: FileId)
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/files/types/File.kt b/core/src/main/kotlin/io/timemates/backend/files/types/File.kt
deleted file mode 100644
index e7342069..00000000
--- a/core/src/main/kotlin/io/timemates/backend/files/types/File.kt
+++ /dev/null
@@ -1,12 +0,0 @@
-package io.timemates.backend.files.types
-
-import io.timemates.backend.files.types.value.FileId
-
-sealed class File {
- abstract val fileId: FileId
-
- /**
- * File with image.
- */
- data class Image(override val fileId: FileId) : File()
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/files/types/FileType.kt b/core/src/main/kotlin/io/timemates/backend/files/types/FileType.kt
deleted file mode 100644
index cf1d8780..00000000
--- a/core/src/main/kotlin/io/timemates/backend/files/types/FileType.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package io.timemates.backend.files.types
-
-enum class FileType {
- IMAGE,
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/files/types/FilesScope.kt b/core/src/main/kotlin/io/timemates/backend/files/types/FilesScope.kt
deleted file mode 100644
index 04b6826e..00000000
--- a/core/src/main/kotlin/io/timemates/backend/files/types/FilesScope.kt
+++ /dev/null
@@ -1,11 +0,0 @@
-package io.timemates.backend.files.types
-
-import io.timemates.backend.features.authorization.Scope
-
-sealed class FilesScope : Scope {
- data object Write : Read()
-
- open class Read : FilesScope() {
- companion object : Read()
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/files/types/value/FileId.kt b/core/src/main/kotlin/io/timemates/backend/files/types/value/FileId.kt
deleted file mode 100644
index 1fd1318b..00000000
--- a/core/src/main/kotlin/io/timemates/backend/files/types/value/FileId.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.timemates.backend.files.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class FileId private constructor(val string: String) {
- companion object : SafeConstructor() {
- const val SIZE = 64
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: String): FileId {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- SIZE -> FileId(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(SIZE))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/files/usecases/GetImageUseCase.kt b/core/src/main/kotlin/io/timemates/backend/files/usecases/GetImageUseCase.kt
deleted file mode 100644
index 62e64a19..00000000
--- a/core/src/main/kotlin/io/timemates/backend/files/usecases/GetImageUseCase.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.timemates.backend.files.usecases
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.files.repositories.FilesRepository
-import io.timemates.backend.files.types.File
-import java.io.InputStream
-
-class GetImageUseCase(
- private val filesRepository: FilesRepository,
-) : UseCase {
- suspend fun execute(file: File): Result {
- return when (val result = filesRepository.retrieve(file)) {
- null -> Result.NotFound
- else -> Result.Success(result)
- }
- }
-
- sealed interface Result {
- @JvmInline
- value class Success(val inputStream: InputStream) : Result
- data object NotFound : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/files/usecases/UploadFileUseCase.kt b/core/src/main/kotlin/io/timemates/backend/files/usecases/UploadFileUseCase.kt
deleted file mode 100644
index 896b157f..00000000
--- a/core/src/main/kotlin/io/timemates/backend/files/usecases/UploadFileUseCase.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package io.timemates.backend.files.usecases
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.validation.createOrThrowInternally
-import com.timemates.random.RandomProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.files.repositories.FilesRepository
-import io.timemates.backend.files.types.FileType
-import io.timemates.backend.files.types.FilesScope
-import io.timemates.backend.files.types.value.FileId
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
-
-class UploadFileUseCase(
- private val files: FilesRepository,
- private val randomProvider: RandomProvider,
- private val timeProvider: TimeProvider,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- fileType: FileType,
- inputStream: Flow,
- ): Result {
- val fileId = FileId.createOrThrowInternally(randomProvider.randomHash(FileId.SIZE))
-
- // saving compressed variants, if exception occurs, we remove all already
- // saved variants
- try {
- coroutineScope {
- files.save(fileId, fileType, inputStream, timeProvider.provide())
- }
- } catch (exception: Exception) {
- files.remove(fileId)
- exception.printStackTrace()
- return Result.Failure
- }
-
- return Result.Success(fileId)
- }
-
- sealed class Result {
- data object Failure : Result()
-
- data class Success(val fileId: FileId) : Result()
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/fsm/ConfirmationState.kt b/core/src/main/kotlin/io/timemates/backend/timers/fsm/ConfirmationState.kt
deleted file mode 100644
index f375c0d4..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/fsm/ConfirmationState.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package io.timemates.backend.timers.fsm
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.fsm.State
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.minutes
-
-data class ConfirmationState(
- override val timerId: TimerId,
- override val timeProvider: TimeProvider,
- override val timersRepository: TimersRepository,
- // timeout by default to avoid dead timers
- override val alive: Duration = 5.minutes,
- override val publishTime: UnixTime,
- override val timerSessionRepository: TimerSessionRepository,
-) : TimerState() {
-
- override suspend fun onEnter(): TimerState = apply {
- timerSessionRepository.setActiveUsersConfirmationRequirement(timerId)
- }
-
- override suspend fun onEvent(event: TimerEvent): TimerState {
- return when (event) {
- is TimerEvent.AttendanceConfirmed -> {
- timerSessionRepository
-
- RunningState(
- publishTime = timeProvider.provide(),
- timerId = timerId,
- timersRepository = timersRepository,
- timeProvider = timeProvider,
- timerSessionRepository = timerSessionRepository,
- )
- }
-
- else -> super.onEvent(event)
- }
- }
-
- override suspend fun onTimeout(): State {
- timerSessionRepository.removeNotConfirmedUsers(timerId)
-
- return InactiveState(
- timerId = timerId,
- publishTime = publishTime,
- timersRepository = timersRepository,
- timerSessionRepository = timerSessionRepository,
- timeProvider = timeProvider,
- )
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/fsm/InactiveState.kt b/core/src/main/kotlin/io/timemates/backend/timers/fsm/InactiveState.kt
deleted file mode 100644
index 23852207..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/fsm/InactiveState.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package io.timemates.backend.timers.fsm
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-import kotlin.time.Duration
-
-class InactiveState(
- override val timerId: TimerId,
- override val publishTime: UnixTime,
- override val timersRepository: TimersRepository,
- override val timerSessionRepository: TimerSessionRepository,
- override val timeProvider: TimeProvider,
-) : TimerState() {
- override val alive: Duration = Duration.INFINITE
-
- override suspend fun onEvent(event: TimerEvent): TimerState {
- return when (event) {
- TimerEvent.Start -> RunningState(
- timerId = timerId,
- timersRepository = timersRepository,
- timerSessionRepository = timerSessionRepository,
- timeProvider = timeProvider,
- alive = alive,
- publishTime = publishTime
- )
-
- TimerEvent.Stop -> this
-
- else -> super.onEvent(event)
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/fsm/PauseState.kt b/core/src/main/kotlin/io/timemates/backend/timers/fsm/PauseState.kt
deleted file mode 100644
index a4624445..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/fsm/PauseState.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.timemates.backend.timers.fsm
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.fsm.State
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-import kotlin.time.Duration
-
-data class PauseState(
- override val timersRepository: TimersRepository,
- override val timeProvider: TimeProvider,
- override val timerId: TimerId,
- override val alive: Duration = Duration.INFINITE,
- override val publishTime: UnixTime,
- override val timerSessionRepository: TimerSessionRepository,
-) : TimerState() {
- override suspend fun onEnter(): State {
- timerSessionRepository.setState(timerId, this)
- return super.onEnter()
- }
-
- override suspend fun onEvent(event: TimerEvent): TimerState {
- return when (event) {
- TimerEvent.Start -> RunningState(
- timerId = timerId,
- timersRepository = timersRepository,
- timeProvider = timeProvider,
- alive = alive,
- publishTime = publishTime,
- timerSessionRepository = timerSessionRepository
- )
-
- else -> super.onEvent(event)
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/fsm/RestState.kt b/core/src/main/kotlin/io/timemates/backend/timers/fsm/RestState.kt
deleted file mode 100644
index f360aa7e..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/fsm/RestState.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-package io.timemates.backend.timers.fsm
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-import kotlin.time.Duration
-
-data class RestState(
- override val timerId: TimerId,
- override val timersRepository: TimersRepository,
- override val timeProvider: TimeProvider,
- override val alive: Duration = Duration.ZERO,
- override val publishTime: UnixTime,
- override val timerSessionRepository: TimerSessionRepository,
-) : TimerState() {
-
- override suspend fun onEnter(): TimerState {
- val settings = timersRepository.getTimerSettings(timerId)!!
-
- return copy(alive = settings.restTime)
- }
-
- override suspend fun onEvent(event: TimerEvent): TimerState {
- return when (event) {
- is TimerEvent.SettingsChanged -> {
- if (event.newSettings.restTime != event.oldSettings.restTime)
- copy(
- alive = event.newSettings.restTime -
- (timeProvider.provide() - publishTime),
- )
- else this
- }
-
- else -> super.onEvent(event)
- }
- }
-
- override suspend fun onTimeout(): TimerState {
- return if (timersRepository.getTimerSettings(timerId)!!.isConfirmationRequired) {
- ConfirmationState(
- timerId = timerId,
- timeProvider = timeProvider,
- publishTime = timeProvider.provide(),
- timersRepository = timersRepository,
- timerSessionRepository = timerSessionRepository,
- )
- } else RunningState(
- timerId = timerId,
- timeProvider = timeProvider,
- publishTime = timeProvider.provide(),
- timersRepository = timersRepository,
- timerSessionRepository = timerSessionRepository,
- )
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/fsm/RunningState.kt b/core/src/main/kotlin/io/timemates/backend/timers/fsm/RunningState.kt
deleted file mode 100644
index 5cba34ec..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/fsm/RunningState.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-package io.timemates.backend.timers.fsm
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-import kotlin.time.Duration
-
-data class RunningState(
- override val timerId: TimerId,
- override val timersRepository: TimersRepository,
- override val timerSessionRepository: TimerSessionRepository,
- override val timeProvider: TimeProvider,
- override val alive: Duration = Duration.ZERO,
- override val publishTime: UnixTime,
-) : TimerState() {
-
- override suspend fun onEnter(): TimerState {
- val settings = timersRepository.getTimerSettings(timerId)!!
- return copy(alive = settings.workTime)
- }
-
- override suspend fun onEvent(event: TimerEvent): TimerState {
- return when (event) {
- TimerEvent.Pause -> PauseState(
- timerId = timerId,
- publishTime = timeProvider.provide(),
- timerSessionRepository = timerSessionRepository,
- timersRepository = timersRepository,
- timeProvider = timeProvider,
- )
-
- is TimerEvent.SettingsChanged -> {
- if (event.newSettings.workTime != event.oldSettings.workTime)
- copy(
- alive = event.newSettings.workTime -
- (timeProvider.provide() - publishTime),
- )
- else this
- }
-
- else -> super.onEvent(event)
- }
- }
-
- override suspend fun onTimeout(): TimerState {
- return RestState(
- timerId = timerId,
- timersRepository = timersRepository,
- timeProvider = timeProvider,
- publishTime = timeProvider.provide(),
- timerSessionRepository = timerSessionRepository,
- )
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/fsm/TimerState.kt b/core/src/main/kotlin/io/timemates/backend/timers/fsm/TimerState.kt
deleted file mode 100644
index af2cf625..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/fsm/TimerState.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.timemates.backend.timers.fsm
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.fsm.State
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-
-/**
- * Base timer state with common-needed providers and repositories.
- * @see SDK Source
- */
-// TODO: separate logic and entity
-sealed class TimerState : State() {
- abstract val timerId: TimerId
- protected abstract val timersRepository: TimersRepository
- protected abstract val timerSessionRepository: TimerSessionRepository
- protected abstract val timeProvider: TimeProvider
-
- override suspend fun onEvent(event: TimerEvent): TimerState {
- return when (event) {
- is TimerEvent.Stop -> InactiveState(
- timerId,
- timeProvider.provide(),
- timersRepository,
- timerSessionRepository,
- timeProvider,
- )
-
- else -> this
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/fsm/TimersStateMachine.kt b/core/src/main/kotlin/io/timemates/backend/timers/fsm/TimersStateMachine.kt
deleted file mode 100644
index 9ff80e6c..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/fsm/TimersStateMachine.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package io.timemates.backend.timers.fsm
-
-import io.timemates.backend.fsm.StateMachine
-import io.timemates.backend.fsm.StateStorage
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-
-/**
- * State machine with states of timers.
- */
-typealias TimersStateMachine = StateMachine
-
-/**
- * Storage with states of timers.
- */
-typealias TimersStateStorage = StateStorage
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/repositories/TimerInvitesRepository.kt b/core/src/main/kotlin/io/timemates/backend/timers/repositories/TimerInvitesRepository.kt
deleted file mode 100644
index 2989edac..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/repositories/TimerInvitesRepository.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package io.timemates.backend.timers.repositories
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.common.markers.Repository
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.timers.types.Invite
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.UserId
-
-interface TimerInvitesRepository : Repository {
- suspend fun getInvites(timerId: TimerId, nextPageToken: PageToken?): Page
- suspend fun removeInvite(timerId: TimerId, code: InviteCode)
- suspend fun getInvite(code: InviteCode): Invite?
-
- suspend fun getInvitesCount(timerId: TimerId, after: UnixTime): Count
-
- suspend fun createInvite(
- timerId: TimerId,
- userId: UserId,
- code: InviteCode,
- creationTime: UnixTime,
- limit: Count,
- )
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/Invite.kt b/core/src/main/kotlin/io/timemates/backend/timers/types/Invite.kt
deleted file mode 100644
index 302ab2fb..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/Invite.kt
+++ /dev/null
@@ -1,13 +0,0 @@
-package io.timemates.backend.timers.types
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.timers.types.value.TimerId
-
-data class Invite(
- val timerId: TimerId,
- val code: InviteCode,
- val creationTime: UnixTime,
- val limit: Count,
-)
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/Timer.kt b/core/src/main/kotlin/io/timemates/backend/timers/types/Timer.kt
deleted file mode 100644
index f45346d6..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/Timer.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.timemates.backend.timers.types
-
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.timers.fsm.TimerState
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.value.TimerDescription
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.timers.types.value.TimerName
-import io.timemates.backend.users.types.value.UserId
-
-data class Timer(
- val id: TimerId,
- val name: TimerName,
- val description: TimerDescription?,
- val ownerId: UserId,
- val settings: TimerSettings,
- val membersCount: Count,
- val state: TimerState,
-)
-
-fun TimersRepository.TimerInformation.toTimer(state: TimerState): Timer {
- return Timer(id, name, description, ownerId, settings, membersCount, state)
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/TimerSettings.kt b/core/src/main/kotlin/io/timemates/backend/timers/types/TimerSettings.kt
deleted file mode 100644
index ab3c83e5..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/TimerSettings.kt
+++ /dev/null
@@ -1,39 +0,0 @@
-package io.timemates.backend.timers.types
-
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.common.markers.TypeDefaults
-import io.timemates.backend.common.types.value.Count
-import kotlin.time.Duration
-import kotlin.time.Duration.Companion.minutes
-
-data class TimerSettings(
- val workTime: Duration,
- val restTime: Duration,
- val bigRestTime: Duration,
- val bigRestEnabled: Boolean,
- val bigRestPer: Count,
- val isEveryoneCanPause: Boolean,
- val isConfirmationRequired: Boolean,
-) {
- companion object : TypeDefaults {
- val Default = TimerSettings(
- workTime = 25.minutes,
- restTime = 5.minutes,
- bigRestTime = 10.minutes,
- bigRestEnabled = true,
- bigRestPer = Count.createOrThrowInternally(4),
- isEveryoneCanPause = false,
- isConfirmationRequired = false,
- )
- }
-
- class Patch(
- val workTime: Duration? = null,
- val restTime: Duration? = null,
- val bigRestTime: Duration? = null,
- val bigRestEnabled: Boolean? = null,
- val bigRestPer: Count? = null,
- val isEveryoneCanPause: Boolean? = null,
- val isConfirmationRequired: Boolean? = null,
- )
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/value/InviteCode.kt b/core/src/main/kotlin/io/timemates/backend/timers/types/value/InviteCode.kt
deleted file mode 100644
index 7ec76688..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/value/InviteCode.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.timemates.backend.timers.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class InviteCode private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
- const val SIZE = 8
-
- context(ValidationFailureHandler)
- override fun create(value: String): InviteCode {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- SIZE -> InviteCode(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(SIZE))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerDescription.kt b/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerDescription.kt
deleted file mode 100644
index d33073a8..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerDescription.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.timers.types.value
-
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class TimerDescription private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
- private val LENGTH_RANGE = 0..200
-
- context(ValidationFailureHandler)
- override fun create(value: String): TimerDescription {
- return when (value.length) {
- in LENGTH_RANGE -> TimerDescription(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(LENGTH_RANGE))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerId.kt b/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerId.kt
deleted file mode 100644
index 26b18c62..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerId.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-package io.timemates.backend.timers.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class TimerId private constructor(val long: Long) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: Long): TimerId {
- return when {
- value > 0 -> TimerId(value)
- else -> onFail(FailureMessage.ofNegative())
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerName.kt b/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerName.kt
deleted file mode 100644
index 64143120..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/value/TimerName.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-package io.timemates.backend.timers.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class TimerName private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
- val LENGTH_RANGE = 3..50
-
- context(ValidationFailureHandler)
- override fun create(value: String): TimerName {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- in LENGTH_RANGE -> TimerName(value)
- else -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(LENGTH_RANGE))
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/GetTimerUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/GetTimerUseCase.kt
deleted file mode 100644
index 963f36b8..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/GetTimerUseCase.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package io.timemates.backend.timers.usecases
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.fsm.getCurrentState
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.Timer
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.toTimer
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
-
-class GetTimerUseCase(
- private val timers: TimersRepository,
- private val sessions: TimerSessionRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- timerId: TimerId,
- ): Result {
- return if (timers.isMemberOf(userId, timerId)) {
- val timer = timers.getTimerInformation(timerId)
- ?.toTimer(sessions.getCurrentState(timerId) ?: return Result.NotFound)
- ?: return Result.NotFound
-
- Result.Success(timer)
- } else {
- Result.NotFound
- }
- }
-
- sealed interface Result {
- @JvmInline
- value class Success(val timer: Timer) : Result
- data object NotFound : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/GetTimersUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/GetTimersUseCase.kt
deleted file mode 100644
index d34d7ff6..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/GetTimersUseCase.kt
+++ /dev/null
@@ -1,54 +0,0 @@
-package io.timemates.backend.timers.usecases
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.fsm.getCurrentState
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.pagination.map
-import io.timemates.backend.pagination.mapIndexed
-import io.timemates.backend.timers.fsm.InactiveState
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.Timer
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.toTimer
-import io.timemates.backend.users.types.value.userId
-
-class GetTimersUseCase(
- private val timers: TimersRepository,
- private val sessionsRepository: TimerSessionRepository,
- private val timeProvider: TimeProvider,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- nextPageToken: PageToken?,
- ): Result {
-
- val infos = timers.getTimersInformation(
- userId, nextPageToken,
- )
-
- val ids = infos.map(TimersRepository.TimerInformation::id)
- val states = ids.map { id -> sessionsRepository.getCurrentState(id) }
-
- return Result.Success(
- infos.mapIndexed { index, information ->
- information.toTimer(
- states.value[index]
- ?: InactiveState(information.id, UnixTime.ZERO, timers, sessionsRepository, timeProvider).also { state ->
- sessionsRepository.setState(information.id, state)
- }
- )
- },
- )
- }
-
- sealed interface Result {
- data class Success(
- val page: Page,
- ) : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/LeaveTimerUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/LeaveTimerUseCase.kt
deleted file mode 100644
index 184f076f..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/LeaveTimerUseCase.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package io.timemates.backend.timers.usecases
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
-
-class LeaveTimerUseCase(
- private val timersRepository: TimersRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- timerId: TimerId,
- ): Result {
- timersRepository.removeMember(userId, timerId)
- return Result.Success
- }
-
- sealed interface Result {
- data object Success : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/RemoveTimerUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/RemoveTimerUseCase.kt
deleted file mode 100644
index cc790981..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/RemoveTimerUseCase.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package io.timemates.backend.timers.usecases
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
-
-class RemoveTimerUseCase(
- private val timers: TimersRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(timerId: TimerId): Result {
- val timer = timers.getTimerInformation(timerId)
- return when {
- timer == null || timer.ownerId != userId -> Result.NotFound
- else -> {
- timers.removeTimer(timerId)
- Result.Success
- }
- }
-
- }
-
- sealed interface Result {
- data object Success : Result
- data object NotFound : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/SetTimerSettingsUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/SetTimerSettingsUseCase.kt
deleted file mode 100644
index 0017d160..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/SetTimerSettingsUseCase.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package io.timemates.backend.timers.usecases
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerSettings
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
-
-class SetTimerSettingsUseCase(
- private val timers: TimersRepository,
- private val sessions: TimerSessionRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- timerId: TimerId,
- newSettings: TimerSettings.Patch,
- ): Result {
- if (timers.getTimerInformation(timerId)?.ownerId != userId)
- return Result.NoAccess
-
- timers.setTimerSettings(
- timerId,
- newSettings
- )
-
- return Result.Success
- }
-
- sealed interface Result {
- data object Success : Result
- data object NoAccess : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/StartTimerUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/StartTimerUseCase.kt
deleted file mode 100644
index 8e32d04d..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/StartTimerUseCase.kt
+++ /dev/null
@@ -1,47 +0,0 @@
-package io.timemates.backend.timers.usecases
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.repositories.canStart
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
-
-class StartTimerUseCase(
- private val timers: TimersRepository,
- private val time: TimeProvider,
- private val sessions: TimerSessionRepository,
-) : UseCase {
-
- context(AuthorizedContext)
- suspend fun execute(timerId: TimerId): Result {
- val timer = timers.getTimerInformation(timerId) ?: return Result.NoAccess
- val settings = timers.getTimerSettings(timerId)!!
- return if (
- (timer.ownerId == userId)
- || (settings.isEveryoneCanPause && timers.isMemberOf(userId, timerId))
- ) {
- if (sessions.canStart(timerId)) {
- sessions.sendEvent(timerId, TimerEvent.Start)
- Result.Success
- } else Result.WrongState
- } else {
- Result.NoAccess
- }
- }
-
- sealed interface Result {
- /**
- * Denotes that the operation was failed due to invalid
- * state that cannot perform the operation due to inconsistency.
- */
- data object WrongState : Result
-
- data object Success : Result
- data object NoAccess : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/StopTimerUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/StopTimerUseCase.kt
deleted file mode 100644
index d45884f1..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/StopTimerUseCase.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package io.timemates.backend.timers.usecases
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.repositories.isRunningState
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
-
-class StopTimerUseCase(
- private val timers: TimersRepository,
- private val sessionRepository: TimerSessionRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- timerId: TimerId,
- ): Result {
- val timer = timers.getTimerInformation(timerId) ?: return Result.NoAccess
- val settings = timers.getTimerSettings(timerId)!!
-
- return if (
- (timer.ownerId == userId)
- || (settings.isEveryoneCanPause && timers.isMemberOf(userId, timerId))
- ) {
- return if (sessionRepository.isRunningState(timerId)) {
- sessionRepository.sendEvent(timerId, TimerEvent.Pause)
- Result.Success
- } else {
- Result.WrongState
- }
- } else {
- Result.NoAccess
- }
- }
-
- sealed interface Result {
- data object Success : Result
- data object NoAccess : Result
- data object WrongState : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/KickTimerUserUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/KickTimerUserUseCase.kt
deleted file mode 100644
index 044d22fd..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/KickTimerUserUseCase.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package io.timemates.backend.timers.usecases.members
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.UserId
-import io.timemates.backend.users.types.value.userId
-
-class KickTimerUserUseCase(
- private val timersRepository: TimersRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- timerId: TimerId,
- userToKick: UserId,
- ): Result {
- if (timersRepository.getTimerInformation(timerId)?.ownerId != userId)
- return Result.NoAccess
-
- timersRepository.removeMember(userToKick, timerId)
- return Result.Success
- }
-
- sealed interface Result {
- data object Success : Result
- data object NoAccess : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/GetInvitesUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/GetInvitesUseCase.kt
deleted file mode 100644
index bd0d3c0a..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/GetInvitesUseCase.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-package io.timemates.backend.timers.usecases.members.invites
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.timers.repositories.TimerInvitesRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.Invite
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
-
-class GetInvitesUseCase(
- private val invites: TimerInvitesRepository,
- private val timers: TimersRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- timerId: TimerId,
- pageToken: PageToken?,
- ): Result {
- if (timers.getTimerInformation(timerId)?.ownerId != userId)
- return Result.NoAccess
-
- return Result.Success(invites.getInvites(timerId, pageToken))
- }
-
- sealed interface Result {
- @JvmInline
- value class Success(val page: Page) : Result
- data object NoAccess : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/JoinByInviteUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/JoinByInviteUseCase.kt
deleted file mode 100644
index 53887462..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/JoinByInviteUseCase.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package io.timemates.backend.timers.usecases.members.invites
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.fsm.getCurrentState
-import io.timemates.backend.timers.repositories.TimerInvitesRepository
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.Timer
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.toTimer
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.users.types.value.userId
-
-class JoinByInviteUseCase(
- private val invites: TimerInvitesRepository,
- private val timers: TimersRepository,
- private val time: TimeProvider,
- private val sessions: TimerSessionRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- code: InviteCode,
- ): Result {
- val invite = invites.getInvite(code) ?: return Result.NotFound
- timers.addMember(userId, invite.timerId, time.provide(), code)
-
- if (invite.limit.int >= timers.getMembersCountOfInvite(invite.timerId, invite.code).int)
- invites.removeInvite(invite.timerId, invite.code)
-
- return Result.Success(
- timers.getTimerInformation(invite.timerId)!!
- .toTimer(sessions.getCurrentState(invite.timerId)!!)
- )
- }
-
- sealed interface Result {
- @JvmInline
- value class Success(val timer: Timer) : Result
- data object NotFound : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/ConfirmStartUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/ConfirmStartUseCase.kt
deleted file mode 100644
index 5b437c12..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/ConfirmStartUseCase.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package io.timemates.backend.timers.usecases.sessions
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.repositories.isConfirmationState
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.users.types.value.userId
-import kotlin.time.Duration.Companion.minutes
-
-class ConfirmStartUseCase(
- private val timers: TimersRepository,
- private val sessions: TimerSessionRepository,
- private val time: TimeProvider,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(): Result {
- val timerId = sessions.getTimerIdOfCurrentSession(userId, time.provide() - 15.minutes)
- ?: return Result.NotFound
-
- if (sessions.isConfirmationState(timerId))
- return Result.WrongState
-
- sessions.sendEvent(timerId, TimerEvent.AttendanceConfirmed(timerId, userId))
- return Result.Success
- }
-
- sealed interface Result {
- data object WrongState : Result
- data object NotFound : Result
- data object Success : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/GetCurrentTimerSessionUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/GetCurrentTimerSessionUseCase.kt
deleted file mode 100644
index 818176c3..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/GetCurrentTimerSessionUseCase.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package io.timemates.backend.timers.usecases.sessions
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.fsm.getCurrentState
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.Timer
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.toTimer
-import io.timemates.backend.users.types.value.userId
-import kotlin.time.Duration.Companion.minutes
-
-class GetCurrentTimerSessionUseCase(
- private val sessionsRepository: TimerSessionRepository,
- private val timers: TimersRepository,
- private val time: TimeProvider,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(): Result {
- return when (val id = sessionsRepository.getTimerIdOfCurrentSession(userId, time.provide() - 15.minutes)) {
- null -> Result.NotFound
-
- else -> {
- Result.Success(
- timers.getTimerInformation(id)!!.toTimer(
- sessionsRepository.getCurrentState(id)!!
- )
- )
- }
- }
- }
-
- sealed interface Result {
- data class Success(
- val timer: Timer
- ) : Result
-
- data object NotFound : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/GetStateUpdatesUseCase.kt b/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/GetStateUpdatesUseCase.kt
deleted file mode 100644
index 9bcacdc5..00000000
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/GetStateUpdatesUseCase.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package io.timemates.backend.timers.usecases.sessions
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.fsm.TimerState
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
-import kotlinx.coroutines.flow.Flow
-
-class GetStateUpdatesUseCase(
- private val timersRepository: TimersRepository,
- private val sessionRepository: TimerSessionRepository,
- private val timeProvider: TimeProvider,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(timerId: TimerId): Result {
- if (!timersRepository.isMemberOf(userId, timerId))
- return Result.NoAccess
-
- return Result.Success(sessionRepository.getState(timerId) ?: return Result.NoAccess)
- }
-
- sealed interface Result {
- @JvmInline
- value class Success(val states: Flow) : Result
- data object NoAccess : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/users/types/Avatar.kt b/core/src/main/kotlin/io/timemates/backend/users/types/Avatar.kt
deleted file mode 100644
index 49485575..00000000
--- a/core/src/main/kotlin/io/timemates/backend/users/types/Avatar.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package io.timemates.backend.users.types
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-sealed interface Avatar {
- @JvmInline
- value class GravatarId private constructor(val string: String): Avatar {
- companion object : SafeConstructor() {
- const val SIZE = 128
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: String): GravatarId {
- return when (value.length) {
- 0 -> onFail(FailureMessage.ofBlank())
- SIZE -> GravatarId(value)
- else -> onFail(FailureMessage.ofSize(SIZE))
- }
- }
- }
- }
-
- @JvmInline
- value class FileId private constructor(val string: String): Avatar {
- companion object : SafeConstructor() {
- const val SIZE = 64
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: String): FileId {
- return when (value.length) {
- 0 -> onFail(FailureMessage.ofBlank())
- SIZE -> FileId(value)
- else -> onFail(FailureMessage.ofSize(SIZE))
- }
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/users/types/User.kt b/core/src/main/kotlin/io/timemates/backend/users/types/User.kt
deleted file mode 100644
index 712c25ac..00000000
--- a/core/src/main/kotlin/io/timemates/backend/users/types/User.kt
+++ /dev/null
@@ -1,18 +0,0 @@
-package io.timemates.backend.users.types
-
-import io.timemates.backend.files.types.value.FileId
-import io.timemates.backend.users.types.value.*
-
-data class User(
- val id: UserId,
- val name: UserName,
- val emailAddress: EmailAddress?,
- val description: UserDescription?,
- val avatar: Avatar?,
-) {
- data class Patch(
- val name: UserName? = null,
- val description: UserDescription? = null,
- val avatar: Avatar?,
- )
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/users/types/value/EmailAddress.kt b/core/src/main/kotlin/io/timemates/backend/users/types/value/EmailAddress.kt
deleted file mode 100644
index a875e46d..00000000
--- a/core/src/main/kotlin/io/timemates/backend/users/types/value/EmailAddress.kt
+++ /dev/null
@@ -1,33 +0,0 @@
-package io.timemates.backend.users.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class EmailAddress private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
- val SIZE = 3..200
- private val emailPattern = Regex(
- "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
- "\\@" +
- "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
- "(" +
- "\\." +
- "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
- ")+"
- )
-
- context(ValidationFailureHandler)
- override fun create(value: String): EmailAddress {
- return when {
- value.isEmpty() -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- value.length !in SIZE -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(SIZE))
- !emailPattern.matches(value) -> onFail(io.timemates.backend.validation.FailureMessage.ofPattern(emailPattern))
- else -> EmailAddress(value)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/users/types/value/UserDescription.kt b/core/src/main/kotlin/io/timemates/backend/users/types/value/UserDescription.kt
deleted file mode 100644
index 8c587138..00000000
--- a/core/src/main/kotlin/io/timemates/backend/users/types/value/UserDescription.kt
+++ /dev/null
@@ -1,26 +0,0 @@
-package io.timemates.backend.users.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class UserDescription private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- /**
- * Size range of the user's short bio.
- */
- private val SIZE = 0..200
-
- context(ValidationFailureHandler)
- override fun create(value: String): UserDescription {
- return when (value.length) {
- !in SIZE -> onFail(FailureMessage.ofSize(SIZE))
- else -> UserDescription(value)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/users/types/value/UserId.kt b/core/src/main/kotlin/io/timemates/backend/users/types/value/UserId.kt
deleted file mode 100644
index fd6f8c80..00000000
--- a/core/src/main/kotlin/io/timemates/backend/users/types/value/UserId.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package io.timemates.backend.users.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.features.authorization.types.AuthorizedId
-import io.timemates.backend.users.types.value.UserId.Companion.asUserId
-
-@JvmInline
-value class UserId private constructor(val long: Long) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- context(ValidationFailureHandler)
- override fun create(value: Long): UserId {
- return when {
- value >= 0 -> UserId(value)
- else -> onFail(FailureMessage.ofNegative())
- }
- }
-
- // optimized way to avoid double-checks
- internal fun AuthorizedId.asUserId() = UserId(long)
- }
-}
-
-context(AuthorizedContext<*>)
-val userId: UserId
- get() = authorization.authorizedId.asUserId()
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/users/types/value/UserName.kt b/core/src/main/kotlin/io/timemates/backend/users/types/value/UserName.kt
deleted file mode 100644
index 61ea7272..00000000
--- a/core/src/main/kotlin/io/timemates/backend/users/types/value/UserName.kt
+++ /dev/null
@@ -1,27 +0,0 @@
-package io.timemates.backend.users.types.value
-
-import io.timemates.backend.validation.FailureMessage
-import io.timemates.backend.validation.SafeConstructor
-import io.timemates.backend.validation.ValidationFailureHandler
-import io.timemates.backend.validation.reflection.wrapperTypeName
-
-@JvmInline
-value class UserName private constructor(val string: String) {
- companion object : SafeConstructor() {
- override val displayName: String by wrapperTypeName()
-
- /**
- * Size range of the user's name.
- */
- private val SIZE = 3..50
-
- context(ValidationFailureHandler)
- override fun create(value: String): UserName {
- return when (value.length) {
- 0 -> onFail(io.timemates.backend.validation.FailureMessage.ofBlank())
- !in SIZE -> onFail(io.timemates.backend.validation.FailureMessage.ofSize(SIZE))
- else -> UserName(value)
- }
- }
- }
-}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/users/usecases/EditUserUseCase.kt b/core/src/main/kotlin/io/timemates/backend/users/usecases/EditUserUseCase.kt
deleted file mode 100644
index 8b4c3e89..00000000
--- a/core/src/main/kotlin/io/timemates/backend/users/usecases/EditUserUseCase.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package io.timemates.backend.users.usecases
-
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.users.repositories.UsersRepository
-import io.timemates.backend.users.types.User
-import io.timemates.backend.users.types.UsersScope
-import io.timemates.backend.users.types.value.userId
-
-class EditUserUseCase(
- private val usersRepository: UsersRepository,
-) : UseCase {
- context(AuthorizedContext)
- suspend fun execute(
- patch: User.Patch,
- ): Result {
- usersRepository.edit(userId, patch)
- return Result.Success
- }
-
- sealed interface Result {
- data object Success : Result
- }
-}
\ No newline at end of file
diff --git a/core/src/test/kotlin/io/timemates/backend/auth/usecases/ConfigureNewAccountUseCaseTest.kt b/core/src/test/kotlin/io/timemates/backend/auth/usecases/ConfigureNewAccountUseCaseTest.kt
deleted file mode 100644
index 35c2a2d6..00000000
--- a/core/src/test/kotlin/io/timemates/backend/auth/usecases/ConfigureNewAccountUseCaseTest.kt
+++ /dev/null
@@ -1,110 +0,0 @@
-package io.timemates.backend.auth.usecases
-
-import com.timemates.backend.time.SystemTimeProvider
-import com.timemates.backend.time.TimeProvider
-import com.timemates.random.SecureRandomProvider
-import io.mockk.coEvery
-import io.mockk.impl.annotations.MockK
-import io.mockk.mockk
-import io.mockk.spyk
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.repositories.VerificationsRepository
-import io.timemates.backend.authorization.types.Verification
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.value.Attempts
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.authorization.types.value.VerificationHash
-import io.timemates.backend.authorization.usecases.ConfigureNewAccountUseCase
-import io.timemates.backend.testing.validation.createOrAssert
-import io.timemates.backend.users.repositories.UsersRepository
-import io.timemates.backend.users.types.value.EmailAddress
-import io.timemates.backend.users.types.value.UserId
-import io.timemates.backend.users.types.value.UserName
-import kotlinx.coroutines.runBlocking
-import org.junit.jupiter.api.BeforeAll
-import kotlin.test.assertIs
-
-//@MockKExtension.CheckUnnecessaryStub
-//@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-//@ExtendWith(MockKExtension::class)
-class ConfigureNewAccountUseCaseTest {
- @MockK
- lateinit var verificationsRepository: VerificationsRepository
-
- @MockK
- lateinit var authorizationsRepository: AuthorizationsRepository
-
- @MockK
- lateinit var usersRepository: UsersRepository
-
- private val timeProvider: TimeProvider = SystemTimeProvider()
-
- private val randomProvider = SecureRandomProvider()
-
- private lateinit var useCase: ConfigureNewAccountUseCase
-
- @BeforeAll
- fun before() {
- useCase = ConfigureNewAccountUseCase(
- usersRepository,
- authorizationsRepository,
- verificationsRepository,
- timeProvider,
- randomProvider
- )
- }
-
- // todo fix when mockk will support value classes in coEvery
- //@Test
- fun `test configure new account`(): Unit = runBlocking {
- // GIVEN
- val clientMetadata = spyk()
- val userId = mockk(relaxed = true)
- val email = EmailAddress.createOrAssert("test@email.com")
- val verificationHash = VerificationHash.createOrAssert(randomProvider.randomHash(VerificationHash.SIZE))
- coEvery { verificationsRepository.getVerification(any()) }
- .returns(
- Verification(
- email,
- VerificationCode.createOrAssert("1234F"),
- Attempts.createOrAssert(3),
- timeProvider.provide(),
- true,
- clientMetadata
- )
- )
- coEvery { usersRepository.createUser(any(), any(), any(), any()) }
- .returns(userId)
-
- // WHEN
- val result = useCase.execute(
- verificationHash,
- UserName.createOrAssert("Test"),
- null
- )
-
- // THEN
- assertIs(value = result)
- }
-
-// @Test
-// fun testNotFound(): Unit = runBlocking {
-//
-//
-//
-// // operation should fail as verification wasn't confirmed
-// assert(
-// useCase.invoke(
-// token, UserName("test"), null
-// ) is ConfigureNewAccountUseCase.Result.NotFound
-// )
-//
-// // should fail as there is no such token
-// assertEquals(
-// actual = useCase.invoke(
-// VerificationHash.createOrAssert("12345"),
-// UserName.createOrAssert("test"), null
-// ) is ConfigureNewAccountUseCase.Result.NotFound
-// )
-// }
-}
\ No newline at end of file
diff --git a/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetAuthorizationsUseCaseTest.kt b/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetAuthorizationsUseCaseTest.kt
deleted file mode 100644
index eac428a6..00000000
--- a/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetAuthorizationsUseCaseTest.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package io.timemates.backend.auth.usecases
-
-import com.timemates.random.SecureRandomProvider
-import io.mockk.MockKAnnotations
-import io.mockk.coEvery
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.usecases.GetAuthorizationsUseCase
-import io.timemates.backend.pagination.Ordering
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.testing.auth.testAuthContext
-import kotlinx.coroutines.runBlocking
-import kotlin.test.assertEquals
-
-// todo correct tests
-//@Testable
-//@TestInstance(TestInstance.Lifecycle.PER_CLASS)
-class GetAuthorizationsUseCaseTest {
-
- private lateinit var useCase: GetAuthorizationsUseCase
- private val randomProvider = SecureRandomProvider()
-
-// @MockK
-private lateinit var authorizationsRepository: AuthorizationsRepository
-
- // @BeforeEach
- fun before() {
- MockKAnnotations.init(this)
- useCase = GetAuthorizationsUseCase(
- authorizationsRepository = authorizationsRepository
- )
- }
-
- // @Test
- fun `test success get authorizations`() = runBlocking {
- val pageToken = PageToken.toGive(randomProvider.randomHash(64))
- val nextPageToken = PageToken.toGive(randomProvider.randomHash(64))
- coEvery { authorizationsRepository.getList(any(), any()) }.returns(
- Page(emptyList(), nextPageToken, Ordering.ASCENDING)
- )
- val result: GetAuthorizationsUseCase.Result = testAuthContext { useCase.execute(pageToken) }
- assertEquals(GetAuthorizationsUseCase.Result.Success(emptyList(), nextPageToken), result)
- }
-}
\ No newline at end of file
diff --git a/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetCurrentTimerSessionUseCaseTest.kt b/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetCurrentTimerSessionUseCaseTest.kt
deleted file mode 100644
index ca2a9edf..00000000
--- a/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetCurrentTimerSessionUseCaseTest.kt
+++ /dev/null
@@ -1,84 +0,0 @@
-package io.timemates.backend.auth.usecases
-
-import com.timemates.backend.time.SystemTimeProvider
-import com.timemates.backend.time.UnixTime
-import io.mockk.coEvery
-import io.mockk.impl.annotations.MockK
-import io.mockk.junit5.MockKExtension
-import io.mockk.mockk
-import io.timemates.backend.testing.auth.testAuthContext
-import io.timemates.backend.testing.validation.createOrAssert
-import io.timemates.backend.timers.fsm.TimerState
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.Timer
-import io.timemates.backend.timers.types.toTimer
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.timers.usecases.sessions.GetCurrentTimerSessionUseCase
-import io.timemates.backend.users.types.value.UserId
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.runBlocking
-import org.junit.jupiter.api.BeforeAll
-import org.junit.jupiter.api.extension.ExtendWith
-import kotlin.test.assertEquals
-
-// todo correct tests
-// @Testable
-// @TestInstance(TestInstance.Lifecycle.PER_CLASS)
-@ExtendWith(MockKExtension::class)
-class GetCurrentTimerSessionUseCaseTest {
- @MockK
- lateinit var sessionsRepository: TimerSessionRepository
-
- @MockK
- lateinit var timers: TimersRepository
-
- private lateinit var useCase: GetCurrentTimerSessionUseCase
-
- private val userId = UserId.createOrAssert(0)
-
- private val timeProvider = SystemTimeProvider()
-
- private val lastActiveTime = UnixTime.ZERO
-
- private val id = TimerId.createOrAssert(1)
-
- @BeforeAll
- fun before() {
- useCase = GetCurrentTimerSessionUseCase(sessionsRepository, timers, timeProvider)
- }
-
- // @Test
- fun `test get current timer session`(): Unit = runBlocking {
- // GIVEN
- val timer = mockk()
-
- coEvery { sessionsRepository.getTimerIdOfCurrentSession(userId, lastActiveTime) } returns id
- coEvery { sessionsRepository.getState(id)!! } returns flowOf(mockk())
- coEvery { timers.getTimerInformation(id)!!.toTimer(mockk()) } returns timer
-
- // WHEN
- val result = testAuthContext { useCase.execute() }
-
- // THEN
- assertEquals(
- expected = GetCurrentTimerSessionUseCase.Result.Success(timer),
- actual = result
- )
- }
-
- // @Test
- fun `test get not found result from get current timer session`(): Unit = runBlocking {
- // GIVEN
- coEvery { sessionsRepository.getTimerIdOfCurrentSession(userId, lastActiveTime) } returns null
-
- // WHEN
- val result = testAuthContext { useCase.execute() }
-
- // THEN
- assertEquals(
- expected = GetCurrentTimerSessionUseCase.Result.NotFound,
- actual = result
- )
- }
-}
\ No newline at end of file
diff --git a/core/types/auth-integration/build.gradle.kts b/core/types/auth-integration/build.gradle.kts
new file mode 100644
index 00000000..ab9fe5dd
--- /dev/null
+++ b/core/types/auth-integration/build.gradle.kts
@@ -0,0 +1,8 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+}
+
+dependencies {
+ implementation(projects.core.types)
+ implementation(projects.foundation.authorization)
+}
\ No newline at end of file
diff --git a/core/types/auth-integration/src/main/kotlin/org/timemates/backend/core/types/integration/auth/AuthorizedExt.kt b/core/types/auth-integration/src/main/kotlin/org/timemates/backend/core/types/integration/auth/AuthorizedExt.kt
new file mode 100644
index 00000000..a402f446
--- /dev/null
+++ b/core/types/auth-integration/src/main/kotlin/org/timemates/backend/core/types/integration/auth/AuthorizedExt.kt
@@ -0,0 +1,9 @@
+package org.timemates.backend.core.types.integration.auth
+
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+
+@OptIn(ValidationDelicateApi::class)
+val Authorized<*>.userId: UserId get() = UserId.createUnsafe(id.long)
\ No newline at end of file
diff --git a/core/types/build.gradle.kts b/core/types/build.gradle.kts
new file mode 100644
index 00000000..2666fa4f
--- /dev/null
+++ b/core/types/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+}
+
+dependencies {
+ api(projects.foundation.time)
+ api(projects.foundation.validation)
+ api(projects.foundation.authorization)
+
+ implementation(projects.foundation.stateMachine)
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/Authorization.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/Authorization.kt
new file mode 100644
index 00000000..a069d579
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/Authorization.kt
@@ -0,0 +1,18 @@
+package org.timemates.backend.types.auth
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.foundation.authorization.Scope
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.users.value.UserId
+
+data class Authorization(
+ val userId: UserId,
+ val accessHash: AccessHash,
+ val refreshAccessHash: RefreshHash,
+ val scopes: List,
+ val expiresAt: UnixTime,
+ val createdAt: UnixTime,
+ val clientMetadata: ClientMetadata,
+)
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/AuthorizationsScope.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/AuthorizationsScope.kt
similarity index 60%
rename from core/src/main/kotlin/io/timemates/backend/authorization/types/AuthorizationsScope.kt
rename to core/types/src/main/kotlin/org/timemates/backend/types/auth/AuthorizationsScope.kt
index a0a244cd..6af8411b 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/AuthorizationsScope.kt
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/AuthorizationsScope.kt
@@ -1,6 +1,6 @@
-package io.timemates.backend.authorization.types
+package org.timemates.backend.types.auth
-import io.timemates.backend.features.authorization.Scope
+import org.timemates.backend.foundation.authorization.Scope
sealed class AuthorizationsScope : Scope {
open class Read : AuthorizationsScope() {
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/types/Email.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/Email.kt
similarity index 52%
rename from core/src/main/kotlin/io/timemates/backend/authorization/types/Email.kt
rename to core/types/src/main/kotlin/org/timemates/backend/types/auth/Email.kt
index f92fe6d6..76f4726a 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/types/Email.kt
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/Email.kt
@@ -1,7 +1,7 @@
-package io.timemates.backend.authorization.types
+package org.timemates.backend.types.auth
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.users.types.value.EmailAddress
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.users.value.EmailAddress
/**
* Class that represents email that sends to user.
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/Verification.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/Verification.kt
new file mode 100644
index 00000000..55e6ac03
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/Verification.kt
@@ -0,0 +1,16 @@
+package org.timemates.backend.types.auth
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.value.Attempts
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.users.value.EmailAddress
+
+data class Verification(
+ val emailAddress: EmailAddress,
+ val code: VerificationCode,
+ val attempts: Attempts,
+ val time: UnixTime,
+ val isConfirmed: Boolean,
+ val clientMetadata: ClientMetadata,
+)
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/exceptions/AuthorizationException.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/exceptions/AuthorizationException.kt
new file mode 100644
index 00000000..c3b820f5
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/exceptions/AuthorizationException.kt
@@ -0,0 +1,22 @@
+package org.timemates.backend.types.auth.exceptions
+
+/**
+ * Represents an exception related to authorization issues.
+ *
+ * @param message A detailed message describing the authorization exception.
+ */
+sealed class AuthorizationException(message: String) : Exception(message)
+
+/**
+ * Exception thrown when no access hash is provided during authorization.
+ */
+class NoAccessHashException : AuthorizationException(
+ message = "No access hash was provided."
+)
+
+/**
+ * Exception thrown when the provided token has expired, been terminated, or is invalid.
+ */
+class InvalidAccessHashException : AuthorizationException(
+ message = "Your token has expired, been terminated, or is simply invalid."
+)
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/ClientMetadata.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/ClientMetadata.kt
new file mode 100644
index 00000000..d8ed7f8f
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/ClientMetadata.kt
@@ -0,0 +1,7 @@
+package org.timemates.backend.types.auth.metadata
+
+data class ClientMetadata(
+ val clientName: org.timemates.backend.types.auth.metadata.value.ClientName,
+ val clientVersion: org.timemates.backend.types.auth.metadata.value.ClientVersion,
+ val clientIpAddress: org.timemates.backend.types.auth.metadata.value.ClientIpAddress,
+)
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientIpAddress.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientIpAddress.kt
new file mode 100644
index 00000000..36289266
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientIpAddress.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.auth.metadata.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class ClientIpAddress private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName by wrapperTypeName()
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ else -> Result.success(ClientIpAddress(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientName.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientName.kt
new file mode 100644
index 00000000..c89c5c53
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientName.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.auth.metadata.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class ClientName private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ else -> Result.success(ClientName(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientVersion.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientVersion.kt
new file mode 100644
index 00000000..43552375
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/metadata/value/ClientVersion.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.auth.metadata.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class ClientVersion private constructor(val double: Double) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: Double): Result {
+ return when {
+ value < 1 -> Result.failure(CreationFailure.ofMin(1))
+ else -> Result.success(ClientVersion(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/AccessHash.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/AccessHash.kt
new file mode 100644
index 00000000..0b04bacc
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/AccessHash.kt
@@ -0,0 +1,21 @@
+package org.timemates.backend.types.auth.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class AccessHash private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+ const val SIZE = 128
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ SIZE -> Result.success(AccessHash(value))
+ else -> Result.failure(CreationFailure.ofSizeExact(SIZE))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/Attempts.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/Attempts.kt
new file mode 100644
index 00000000..78731613
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/Attempts.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.auth.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class Attempts private constructor(val int: Int) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: Int): Result {
+ return when {
+ value < 0 -> Result.failure(CreationFailure.ofMin(0))
+ else -> Result.success(Attempts(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/AuthorizationId.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/AuthorizationId.kt
new file mode 100644
index 00000000..89ea1ad3
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/AuthorizationId.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.auth.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class AuthorizationId private constructor(val id: Int) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: Int): Result {
+ return when {
+ value < 0 -> Result.failure(CreationFailure.ofMin(0))
+ else -> Result.success(AuthorizationId(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/RefreshHash.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/RefreshHash.kt
new file mode 100644
index 00000000..3263aec7
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/RefreshHash.kt
@@ -0,0 +1,21 @@
+package org.timemates.backend.types.auth.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class RefreshHash private constructor(val string: String) {
+ companion object : SafeConstructor {
+ const val SIZE = 128
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ SIZE -> Result.success(RefreshHash(value))
+ else -> Result.failure(CreationFailure.ofSizeExact(SIZE))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/VerificationCode.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/VerificationCode.kt
new file mode 100644
index 00000000..855bcd0b
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/VerificationCode.kt
@@ -0,0 +1,21 @@
+package org.timemates.backend.types.auth.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class VerificationCode private constructor(val string: String) {
+ companion object : SafeConstructor {
+ const val SIZE = 8
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ SIZE -> Result.success(VerificationCode(value))
+ else -> Result.failure(CreationFailure.ofSizeExact(SIZE))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/VerificationHash.kt b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/VerificationHash.kt
new file mode 100644
index 00000000..8c824aed
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/auth/value/VerificationHash.kt
@@ -0,0 +1,21 @@
+package org.timemates.backend.types.auth.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class VerificationHash private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ const val SIZE = 128
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ SIZE -> Result.success(VerificationHash(value))
+ else -> Result.failure(CreationFailure.ofSizeExact(SIZE))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/common/value/Count.kt b/core/types/src/main/kotlin/org/timemates/backend/types/common/value/Count.kt
new file mode 100644
index 00000000..1907b8b9
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/common/value/Count.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.common.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class Count private constructor(val int: Int) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: Int): Result {
+ return when {
+ value >= 0 -> Result.success(Count(value))
+ else -> Result.failure(CreationFailure.ofMin(0))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/common/value/Offset.kt b/core/types/src/main/kotlin/org/timemates/backend/types/common/value/Offset.kt
new file mode 100644
index 00000000..7b488f1b
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/common/value/Offset.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.common.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class Offset private constructor(val long: Long) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: Long): Result {
+ return when {
+ value > 0 -> Result.success(Offset(value))
+ else -> Result.failure(CreationFailure.ofMin(0))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/timers/Invite.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/Invite.kt
new file mode 100644
index 00000000..2b991c15
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/Invite.kt
@@ -0,0 +1,13 @@
+package org.timemates.backend.types.timers
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.value.InviteCode
+import org.timemates.backend.types.timers.value.TimerId
+
+data class Invite(
+ val timerId: TimerId,
+ val code: InviteCode,
+ val creationTime: UnixTime,
+ val limit: Count,
+)
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/timers/Timer.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/Timer.kt
new file mode 100644
index 00000000..fdf68a6e
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/Timer.kt
@@ -0,0 +1,17 @@
+package org.timemates.backend.types.timers
+
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.value.TimerDescription
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.timers.value.TimerName
+import org.timemates.backend.types.users.value.UserId
+
+data class Timer(
+ val id: TimerId,
+ val name: TimerName,
+ val description: TimerDescription?,
+ val ownerId: UserId,
+ val settings: TimerSettings,
+ val membersCount: Count,
+ val state: TimerState,
+)
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/TimerEvent.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerEvent.kt
similarity index 78%
rename from core/src/main/kotlin/io/timemates/backend/timers/types/TimerEvent.kt
rename to core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerEvent.kt
index afd7dd4b..b00716ac 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/TimerEvent.kt
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerEvent.kt
@@ -1,7 +1,7 @@
-package io.timemates.backend.timers.types
+package org.timemates.backend.types.timers
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.UserId
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.value.UserId
sealed class TimerEvent {
data object Stop : TimerEvent()
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerSettings.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerSettings.kt
new file mode 100644
index 00000000..bd98b591
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerSettings.kt
@@ -0,0 +1,30 @@
+package org.timemates.backend.types.timers
+
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+
+data class TimerSettings @OptIn(ValidationDelicateApi::class) constructor(
+ val workTime: Duration = 25.minutes,
+ val restTime: Duration = 5.minutes,
+ val bigRestTime: Duration = 10.minutes,
+ val bigRestEnabled: Boolean = true,
+ val bigRestPer: org.timemates.backend.types.common.value.Count = org.timemates.backend.types.common.value.Count.createUnsafe(4),
+ val isEveryoneCanPause: Boolean = false,
+ val isConfirmationRequired: Boolean = false,
+) {
+ companion object {
+ val Default = TimerSettings()
+ }
+
+ class Patch(
+ val workTime: Duration? = null,
+ val restTime: Duration? = null,
+ val bigRestTime: Duration? = null,
+ val bigRestEnabled: Boolean? = null,
+ val bigRestPer: org.timemates.backend.types.common.value.Count? = null,
+ val isEveryoneCanPause: Boolean? = null,
+ val isConfirmationRequired: Boolean? = null,
+ )
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerState.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerState.kt
new file mode 100644
index 00000000..42c49797
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerState.kt
@@ -0,0 +1,91 @@
+package org.timemates.backend.types.timers
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.fsm.State
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.minutes
+
+/**
+ * Sealed class representing different states of a TimeMates entity.
+ *
+ * @property endsAt The time when the state will lose its actuality, if applicable.
+ * @property publishTime The time when the state was published. It's especially useful if the state
+ * was partially updated (for example, settings were changed, and the running state
+ * was adjusted to these settings).
+ */
+sealed interface TimerState : State {
+ override val alive: Duration
+ override val publishTime: UnixTime
+
+ /**
+ * Represents a paused state of the TimeMates entity.
+ * Paused states do not have an exact time to be expired and are usually paused by force
+ * for an indefinite amount of time. They can be resumed only on purpose. The server may
+ * decide to expire paused states after some time, but the client shouldn't focus on that
+ * and should handle the state accordingly.
+ *
+ * @property publishTime The time when the paused state was published.
+ */
+ data class Paused(
+ override val publishTime: UnixTime,
+ override val alive: Duration = 15.minutes,
+ ) : TimerState {
+ override val key: State.Key<*> get() = Key
+
+ companion object Key : State.Key
+ }
+
+ data class ConfirmationWaiting(
+ override val publishTime: UnixTime,
+ override val alive: Duration,
+ ) : TimerState {
+ override val key: State.Key<*> get() = Key
+
+ companion object Key : State.Key
+ }
+
+ /**
+ * Represents an inactive state of the TimeMates entity.
+ *
+ * @property publishTime The time when the inactive state was published.
+ */
+ data class Inactive(
+ override val publishTime: UnixTime,
+ ) : TimerState {
+ override val alive: Duration = Duration.INFINITE
+
+ override val key: State.Key<*> get() = Key
+
+ companion object Key : State.Key
+ }
+
+ /**
+ * Represents a running state of the TimeMates entity.
+ *
+ * @property endsAt The time when the running state will lose its actuality.
+ * @property publishTime The time when the running state was published.
+ */
+ data class Running(
+ override val publishTime: UnixTime,
+ override val alive: Duration,
+ ) : TimerState {
+ override val key: State.Key<*> get() = Key
+
+ companion object Key : State.Key
+ }
+
+ /**
+ * Represents a rest state of the TimeMates entity.
+ *
+ * @property endsAt The time when the rest state will lose its actuality.
+ * @property publishTime The time when the rest state was published.
+ */
+ data class Rest(
+ override val publishTime: UnixTime,
+ override val alive: Duration,
+ ) : TimerState {
+ override val key: State.Key<*> get() = Key
+
+ companion object Key : State.Key
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/TimerUpdate.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerUpdate.kt
similarity index 87%
rename from core/src/main/kotlin/io/timemates/backend/timers/types/TimerUpdate.kt
rename to core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerUpdate.kt
index 988f4431..83141d91 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/TimerUpdate.kt
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimerUpdate.kt
@@ -1,7 +1,6 @@
-package io.timemates.backend.timers.types
+package org.timemates.backend.types.timers
-import io.timemates.backend.timers.fsm.TimerState
-import io.timemates.backend.timers.types.value.TimerId
+import org.timemates.backend.types.timers.value.TimerId
/**
* Class that represents the update of the timer.
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/types/TimersScope.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimersScope.kt
similarity index 58%
rename from core/src/main/kotlin/io/timemates/backend/timers/types/TimersScope.kt
rename to core/types/src/main/kotlin/org/timemates/backend/types/timers/TimersScope.kt
index ee8354ad..3c43e75d 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/types/TimersScope.kt
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/TimersScope.kt
@@ -1,6 +1,6 @@
-package io.timemates.backend.timers.types
+package org.timemates.backend.types.timers
-import io.timemates.backend.features.authorization.Scope
+import org.timemates.backend.foundation.authorization.Scope
sealed class TimersScope : Scope {
data object Write : Read()
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/InviteCode.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/InviteCode.kt
new file mode 100644
index 00000000..da277418
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/InviteCode.kt
@@ -0,0 +1,21 @@
+package org.timemates.backend.types.timers.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class InviteCode private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+ const val SIZE = 8
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ SIZE -> Result.success(InviteCode(value))
+ else -> Result.failure(CreationFailure.ofSizeExact(SIZE))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerDescription.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerDescription.kt
new file mode 100644
index 00000000..b5b02995
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerDescription.kt
@@ -0,0 +1,20 @@
+package org.timemates.backend.types.timers.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class TimerDescription private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+ private val LENGTH_RANGE = 0..200
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ in LENGTH_RANGE -> Result.success(TimerDescription(value))
+ else -> Result.failure(CreationFailure.ofSizeRange(LENGTH_RANGE))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerId.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerId.kt
new file mode 100644
index 00000000..367a62bc
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerId.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.timers.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class TimerId private constructor(val long: Long) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: Long): Result {
+ return when {
+ value > 0 -> Result.success(TimerId(value))
+ else -> Result.failure(CreationFailure.ofMin(0))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerName.kt b/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerName.kt
new file mode 100644
index 00000000..2e186af0
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/timers/value/TimerName.kt
@@ -0,0 +1,21 @@
+package org.timemates.backend.types.timers.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class TimerName private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+ val LENGTH_RANGE = 3..50
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ in LENGTH_RANGE -> Result.success(TimerName(value))
+ else -> Result.failure(CreationFailure.ofSizeRange(LENGTH_RANGE))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/users/Avatar.kt b/core/types/src/main/kotlin/org/timemates/backend/types/users/Avatar.kt
new file mode 100644
index 00000000..266e828b
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/users/Avatar.kt
@@ -0,0 +1,39 @@
+package org.timemates.backend.types.users
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+sealed interface Avatar {
+ @JvmInline
+ value class GravatarId private constructor(val string: String) : Avatar {
+ companion object : SafeConstructor {
+ const val SIZE = 128
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ SIZE -> Result.success(GravatarId(value))
+ else -> Result.failure(CreationFailure.ofSizeExact(SIZE))
+ }
+ }
+ }
+ }
+
+ @JvmInline
+ value class FileId private constructor(val string: String) : Avatar {
+ companion object : SafeConstructor {
+ const val SIZE = 64
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ SIZE -> Result.success(FileId(value))
+ else -> Result.failure(CreationFailure.ofSizeExact(SIZE))
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/users/User.kt b/core/types/src/main/kotlin/org/timemates/backend/types/users/User.kt
new file mode 100644
index 00000000..ce183c41
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/users/User.kt
@@ -0,0 +1,19 @@
+package org.timemates.backend.types.users
+
+import org.timemates.backend.types.users.value.UserDescription
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.types.users.value.UserName
+
+data class User(
+ val id: UserId,
+ val name: UserName,
+ val emailAddress: org.timemates.backend.types.users.value.EmailAddress?,
+ val description: UserDescription?,
+ val avatar: Avatar?,
+) {
+ data class Patch(
+ val name: UserName? = null,
+ val description: UserDescription? = null,
+ val avatar: Avatar?,
+ )
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/users/types/UsersScope.kt b/core/types/src/main/kotlin/org/timemates/backend/types/users/UsersScope.kt
similarity index 58%
rename from core/src/main/kotlin/io/timemates/backend/users/types/UsersScope.kt
rename to core/types/src/main/kotlin/org/timemates/backend/types/users/UsersScope.kt
index 99e3da10..f4de6e1a 100644
--- a/core/src/main/kotlin/io/timemates/backend/users/types/UsersScope.kt
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/users/UsersScope.kt
@@ -1,6 +1,6 @@
-package io.timemates.backend.users.types
+package org.timemates.backend.types.users
-import io.timemates.backend.features.authorization.Scope
+import org.timemates.backend.foundation.authorization.Scope
sealed class UsersScope : Scope {
open class Read : UsersScope() {
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/users/value/EmailAddress.kt b/core/types/src/main/kotlin/org/timemates/backend/types/users/value/EmailAddress.kt
new file mode 100644
index 00000000..6e2b19bc
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/users/value/EmailAddress.kt
@@ -0,0 +1,31 @@
+package org.timemates.backend.types.users.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class EmailAddress private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+ val SIZE = 3..200
+ private val emailPattern = Regex(
+ "[a-zA-Z0-9\\+\\.\\_\\%\\-\\+]{1,256}" +
+ "\\@" +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,64}" +
+ "(" +
+ "\\." +
+ "[a-zA-Z0-9][a-zA-Z0-9\\-]{0,25}" +
+ ")+"
+ )
+
+ override fun create(value: String): Result {
+ return when {
+ value.isEmpty() -> Result.failure(CreationFailure.ofBlank())
+ value.length !in SIZE -> Result.failure(CreationFailure.ofSizeRange(SIZE))
+ !emailPattern.matches(value) -> Result.failure(CreationFailure.ofPattern(emailPattern))
+ else -> Result.success(EmailAddress(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserDescription.kt b/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserDescription.kt
new file mode 100644
index 00000000..3d05984b
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserDescription.kt
@@ -0,0 +1,24 @@
+package org.timemates.backend.types.users.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class UserDescription private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ /**
+ * Size range of the user's short bio.
+ */
+ private val SIZE = 0..200
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ !in SIZE -> Result.failure(CreationFailure.ofSizeRange(SIZE))
+ else -> Result.success(UserDescription(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserId.kt b/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserId.kt
new file mode 100644
index 00000000..991a1afc
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserId.kt
@@ -0,0 +1,23 @@
+package org.timemates.backend.types.users.value
+
+import org.timemates.backend.foundation.authorization.types.AuthorizedId
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class UserId private constructor(val long: Long) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ override fun create(value: Long): Result {
+ return when {
+ value >= 0 -> Result.success(UserId(value))
+ else -> Result.failure(CreationFailure.ofMin(0))
+ }
+ }
+
+ // optimized way to avoid double-checks
+ internal fun AuthorizedId.asUserId() = UserId(long)
+ }
+}
\ No newline at end of file
diff --git a/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserName.kt b/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserName.kt
new file mode 100644
index 00000000..a39d1b4b
--- /dev/null
+++ b/core/types/src/main/kotlin/org/timemates/backend/types/users/value/UserName.kt
@@ -0,0 +1,25 @@
+package org.timemates.backend.types.users.value
+
+import org.timemates.backend.validation.CreationFailure
+import org.timemates.backend.validation.SafeConstructor
+import org.timemates.backend.validation.reflection.wrapperTypeName
+
+@JvmInline
+value class UserName private constructor(val string: String) {
+ companion object : SafeConstructor {
+ override val displayName: String by wrapperTypeName()
+
+ /**
+ * Size range of the user's name.
+ */
+ private val SIZE = 3..50
+
+ override fun create(value: String): Result {
+ return when (value.length) {
+ 0 -> Result.failure(CreationFailure.ofBlank())
+ !in SIZE -> Result.failure(CreationFailure.ofSizeRange(SIZE))
+ else -> Result.success(UserName(value))
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/README.md b/data/README.md
deleted file mode 100644
index 81ddda29..00000000
--- a/data/README.md
+++ /dev/null
@@ -1,28 +0,0 @@
-## Data Module
-
-The `data` module is responsible for handling data access and interacting with external data sources in the application.
-
-### Purpose
-
-The primary purpose of the `data` module is to encapsulate the implementation of repositories. It provides an abstraction layer between the domain layer and the underlying data sources, such as databases,
-caches, or external services.
-
-### Implementation Details
-
-The `:data` module may include various implementations based on the specific data storage technologies and frameworks
-used in the application. These implementations could include database-specific code, caching mechanisms, or
-integrations with external services.
-
-#### Example
-
-Here's an example of a `UsersRepository` implementation within the `:data` module:
-
-```kotlin
-class PostgresqlUsersRepository(/* ... */) : UsersRepository {
- override suspend fun getUser(userId: UserId): User {
- return // ...
- }
-
- // ...
-}
-```
\ No newline at end of file
diff --git a/data/build.gradle.kts b/data/build.gradle.kts
deleted file mode 100644
index fae80215..00000000
--- a/data/build.gradle.kts
+++ /dev/null
@@ -1,37 +0,0 @@
-plugins {
- id(libs.plugins.jvm.module.convention.get().pluginId)
- alias(libs.plugins.kotlinx.serialization)
-}
-
-dependencies {
- implementation(projects.core)
- implementation(projects.common.exposedUtils)
- implementation(projects.common.stateMachine)
- implementation(projects.common.pageToken)
- implementation(projects.common.hashing)
- testImplementation(projects.common.testUtils)
-
- implementation(libs.ktor.client.core)
- implementation(libs.ktor.client.cio)
- implementation(libs.ktor.client.content.negotiation)
- implementation(libs.ktor.client.logging)
- implementation(libs.ktor.json)
-
- implementation(projects.common.smtpMailer)
-
- implementation(libs.kotlinx.serialization.json)
-
- implementation(libs.exposed.core)
- implementation(libs.exposed.jdbc)
-
- implementation(libs.cache4k)
-
- implementation(libs.kotlinx.coroutines)
-
- implementation(libs.commons.io)
-
- testImplementation(libs.h2.database)
- testImplementation(libs.kotlin.test)
- testImplementation(libs.junit.jupiter)
- testImplementation(libs.mockk)
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlVerificationsRepository.kt b/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlVerificationsRepository.kt
deleted file mode 100644
index 58d67d8f..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlVerificationsRepository.kt
+++ /dev/null
@@ -1,65 +0,0 @@
-package io.timemates.backend.data.authorization
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.authorization.repositories.VerificationsRepository
-import io.timemates.backend.authorization.types.Verification
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.value.Attempts
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.authorization.types.value.VerificationHash
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.data.authorization.db.TableVerificationsDataSource
-import io.timemates.backend.data.authorization.mapper.VerificationsMapper
-import io.timemates.backend.users.types.value.EmailAddress
-
-class PostgresqlVerificationsRepository(
- private val dbVerifications: TableVerificationsDataSource,
- private val mapper: VerificationsMapper,
-) : VerificationsRepository {
- override suspend fun save(
- emailAddress: EmailAddress,
- verificationToken: VerificationHash,
- code: VerificationCode,
- time: UnixTime,
- attempts: Attempts,
- clientMetadata: ClientMetadata,
- ) {
- dbVerifications.add(
- emailAddress.string,
- verificationToken.string,
- code.string,
- time.inMilliseconds,
- attempts.int,
- clientMetadata.clientName.string,
- clientMetadata.clientVersion.double,
- clientMetadata.clientIpAddress.string
- )
- }
-
- override suspend fun addAttempt(verificationToken: VerificationHash) {
- dbVerifications.decreaseAttempts(verificationToken.string)
- }
-
- override suspend fun getVerification(verificationToken: VerificationHash): Verification? {
- return dbVerifications.getVerification(verificationToken.string)?.let(mapper::dbToDomain)
- }
-
- override suspend fun remove(verificationToken: VerificationHash) {
- dbVerifications.remove(verificationToken.string)
- }
-
- override suspend fun getNumberOfAttempts(emailAddress: EmailAddress, after: UnixTime): Count {
- return dbVerifications.getAttempts(emailAddress.string, after.inMilliseconds)
- .let { Count.createOrThrowInternally(it) }
- }
-
- override suspend fun getNumberOfSessions(emailAddress: EmailAddress, after: UnixTime): Count {
- return dbVerifications.getSessionsCount(emailAddress.string, after.inMilliseconds)
- .let { Count.createOrThrowInternally(it) }
- }
-
- override suspend fun markConfirmed(verificationToken: VerificationHash) {
- dbVerifications.setAsConfirmed(verificationToken.string)
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/mapper/AuthorizationsMapper.kt b/data/src/main/kotlin/io/timemates/backend/data/authorization/mapper/AuthorizationsMapper.kt
deleted file mode 100644
index 4fa799e0..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/mapper/AuthorizationsMapper.kt
+++ /dev/null
@@ -1,169 +0,0 @@
-package io.timemates.backend.data.authorization.mapper
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.AuthorizationsScope
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.metadata.value.ClientIpAddress
-import io.timemates.backend.authorization.types.metadata.value.ClientName
-import io.timemates.backend.authorization.types.metadata.value.ClientVersion
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.data.authorization.cache.entities.CacheAuthorization
-import io.timemates.backend.data.authorization.db.entities.DbAuthorization
-import io.timemates.backend.data.common.markers.Mapper
-import io.timemates.backend.features.authorization.Scope
-import io.timemates.backend.files.types.FilesScope
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.users.types.UsersScope
-import io.timemates.backend.users.types.value.UserId
-import io.timemates.backend.data.authorization.cache.entities.CacheAuthorization.Permissions.GrantLevel as CacheGrantLevel
-import io.timemates.backend.data.authorization.db.entities.DbAuthorization.Permissions.GrantLevel as DbGrantLevel
-
-class AuthorizationsMapper : Mapper {
- fun dbAuthToDomainAuth(auth: DbAuthorization): Authorization = with(auth) {
- return Authorization(
- userId = UserId.createOrThrowInternally(auth.userId),
- accessHash = AccessHash.createOrThrowInternally(auth.accessHash),
- refreshAccessHash = RefreshHash.createOrThrowInternally(auth.refreshAccessHash),
- scopes = dbPermissionsToDomain(permissions),
- expiresAt = UnixTime.createOrThrowInternally(expiresAt),
- createdAt = UnixTime.createOrThrowInternally(createdAt),
- clientMetadata = ClientMetadata(
- clientName = ClientName.createOrThrowInternally(auth.metaClientName),
- clientVersion = ClientVersion.createOrThrowInternally(auth.metaClientVersion),
- clientIpAddress = ClientIpAddress.createOrThrowInternally(auth.metaClientIpAddress),
- )
- )
- }
-
- fun dbAuthToCacheAuth(auth: DbAuthorization): CacheAuthorization = with(auth) {
- return CacheAuthorization(
- userId,
- accessHash,
- refreshAccessHash,
- dbPermissionsToCachePermissions(permissions),
- expiresAt,
- createdAt,
- ClientMetadata(
- clientName = ClientName.createOrThrowInternally(auth.metaClientName),
- clientVersion = ClientVersion.createOrThrowInternally(auth.metaClientVersion),
- clientIpAddress = ClientIpAddress.createOrThrowInternally(auth.metaClientIpAddress),
- )
- )
- }
-
- private fun dbPermissionsToCachePermissions(
- auth: DbAuthorization.Permissions,
- ): CacheAuthorization.Permissions = with(auth) {
- return CacheAuthorization.Permissions(
- dbGrantLevelToCacheGrantLevel(authorization),
- dbGrantLevelToCacheGrantLevel(users),
- dbGrantLevelToCacheGrantLevel(timers),
- dbGrantLevelToCacheGrantLevel(files),
- )
- }
-
- private fun dbGrantLevelToCacheGrantLevel(
- level: DbAuthorization.Permissions.GrantLevel,
- ): CacheAuthorization.Permissions.GrantLevel {
- return when (level) {
- DbAuthorization.Permissions.GrantLevel.READ -> CacheAuthorization.Permissions.GrantLevel.READ
- DbAuthorization.Permissions.GrantLevel.WRITE -> CacheAuthorization.Permissions.GrantLevel.WRITE
- DbAuthorization.Permissions.GrantLevel.NOT_GRANTED -> CacheAuthorization.Permissions.GrantLevel.NOT_GRANTED
- }
- }
-
- fun cacheAuthToDomainAuth(auth: CacheAuthorization): Authorization = with(auth) {
- return Authorization(
- userId = UserId.createOrThrowInternally(auth.userId),
- accessHash = AccessHash.createOrThrowInternally(auth.accessHash),
- refreshAccessHash = RefreshHash.createOrThrowInternally(auth.refreshAccessHash),
- scopes = cachePermissionsToDomain(permissions),
- expiresAt = UnixTime.createOrThrowInternally(expiresAt),
- createdAt = UnixTime.createOrThrowInternally(createdAt),
- clientMetadata = ClientMetadata(
- clientName = ClientName.createOrThrowInternally(auth.clientMetadata.clientName.string),
- clientVersion = ClientVersion.createOrThrowInternally(auth.clientMetadata.clientVersion.double),
- clientIpAddress = ClientIpAddress.createOrThrowInternally(auth.clientMetadata.clientIpAddress.string),
- )
- )
- }
-
- private fun dbPermissionsToDomain(
- dbPermissions: DbAuthorization.Permissions,
- ): List = with(dbPermissions) {
- return buildList {
- if (authorization != DbGrantLevel.NOT_GRANTED) {
- add(
- if (authorization == DbGrantLevel.WRITE)
- AuthorizationsScope.Write
- else AuthorizationsScope.Read
- )
- }
-
- if (users != DbGrantLevel.NOT_GRANTED) {
- add(
- if (users == DbGrantLevel.WRITE)
- UsersScope.Write
- else UsersScope.Read
- )
- }
-
- if (files != DbGrantLevel.NOT_GRANTED) {
- add(
- if (files == DbGrantLevel.WRITE)
- FilesScope.Write
- else FilesScope.Read
- )
- }
-
- if (timers != DbGrantLevel.NOT_GRANTED) {
- add(
- if (timers == DbGrantLevel.WRITE)
- TimersScope.Write
- else TimersScope.Read
- )
- }
- }
- }
-
- private fun cachePermissionsToDomain(
- cache: CacheAuthorization.Permissions,
- ): List = with(cache) {
- return buildList {
- if (authorization != CacheGrantLevel.NOT_GRANTED) {
- add(
- if (authorization == CacheGrantLevel.WRITE)
- AuthorizationsScope.Write
- else AuthorizationsScope.Read
- )
- }
-
- if (users != CacheGrantLevel.NOT_GRANTED) {
- add(
- if (users == CacheGrantLevel.WRITE)
- UsersScope.Write
- else UsersScope.Read
- )
- }
-
- if (files != CacheGrantLevel.NOT_GRANTED) {
- add(
- if (files == CacheGrantLevel.WRITE)
- FilesScope.Write
- else FilesScope.Read
- )
- }
-
- if (timers != CacheGrantLevel.NOT_GRANTED) {
- add(
- if (timers == CacheGrantLevel.WRITE)
- TimersScope.Write
- else TimersScope.Read
- )
- }
- }
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/mapper/VerificationsMapper.kt b/data/src/main/kotlin/io/timemates/backend/data/authorization/mapper/VerificationsMapper.kt
deleted file mode 100644
index cf7de59c..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/mapper/VerificationsMapper.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-package io.timemates.backend.data.authorization.mapper
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.authorization.types.Verification
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.metadata.value.ClientIpAddress
-import io.timemates.backend.authorization.types.metadata.value.ClientName
-import io.timemates.backend.authorization.types.metadata.value.ClientVersion
-import io.timemates.backend.authorization.types.value.Attempts
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.data.authorization.db.entities.DbVerification
-import io.timemates.backend.data.common.markers.Mapper
-import io.timemates.backend.users.types.value.EmailAddress
-
-class VerificationsMapper : Mapper {
- fun dbToDomain(dbVerification: DbVerification): Verification {
- return Verification(
- emailAddress = EmailAddress.createOrThrowInternally(dbVerification.emailAddress),
- code = VerificationCode.createOrThrowInternally(dbVerification.code),
- attempts = Attempts.createOrThrowInternally(dbVerification.attempts),
- time = UnixTime.createOrThrowInternally(dbVerification.time),
- isConfirmed = dbVerification.isConfirmed,
- clientMetadata = ClientMetadata(
- clientName = ClientName.createOrThrowInternally(dbVerification.metaClientName),
- clientVersion = ClientVersion.createOrThrowInternally(dbVerification.metaClientVersion),
- clientIpAddress = ClientIpAddress.createOrThrowInternally(dbVerification.metaClientIpAddress),
- )
- )
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/common/markers/Mapper.kt b/data/src/main/kotlin/io/timemates/backend/data/common/markers/Mapper.kt
deleted file mode 100644
index 1bfd28c6..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/common/markers/Mapper.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-package io.timemates.backend.data.common.markers
-
-import io.timemates.backend.validation.markers.InternalThrowAbility
-
-interface Mapper : InternalThrowAbility
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/common/repositories/SMTPEmailsRepository.kt b/data/src/main/kotlin/io/timemates/backend/data/common/repositories/SMTPEmailsRepository.kt
deleted file mode 100644
index 47da3360..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/common/repositories/SMTPEmailsRepository.kt
+++ /dev/null
@@ -1,25 +0,0 @@
-package io.timemates.backend.data.common.repositories
-
-import io.timemates.backend.authorization.types.Email
-import io.timemates.backend.common.repositories.EmailsRepository
-import io.timemates.backend.mailer.SMTPMailer
-import io.timemates.backend.users.types.value.EmailAddress
-
-class SMTPEmailsRepository(private val mailer: SMTPMailer) : EmailsRepository {
-
- // TODO better email view experience
- override suspend fun send(emailAddress: EmailAddress, email: Email): Boolean {
- return when (email) {
- is Email.AuthorizeEmail -> try {
- mailer.send(
- address = emailAddress.string,
- subject = "Confirm your authorization",
- body = "Your confirmation code is ${email.code}.",
- )
- } catch (t: Throwable) {
- t.printStackTrace()
- false
- }
- }
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/files/FilesRepository.kt b/data/src/main/kotlin/io/timemates/backend/data/files/FilesRepository.kt
deleted file mode 100644
index 43b014d3..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/files/FilesRepository.kt
+++ /dev/null
@@ -1,41 +0,0 @@
-package io.timemates.backend.data.files
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.data.files.datasource.FileEntityMapper
-import io.timemates.backend.data.files.datasource.LocalFilesDataSource
-import io.timemates.backend.data.files.datasource.PostgresqlFilesDataSource
-import io.timemates.backend.files.types.File
-import io.timemates.backend.files.types.FileType
-import io.timemates.backend.files.types.value.FileId
-import kotlinx.coroutines.flow.Flow
-import java.io.InputStream
-import kotlin.io.path.pathString
-import io.timemates.backend.files.repositories.FilesRepository as FilesRepositoryContract
-
-
-class FilesRepository(
- private val localFilesDataSource: LocalFilesDataSource,
- private val postgresqlFilesDataSource: PostgresqlFilesDataSource,
- private val mapper: FileEntityMapper,
-) : FilesRepositoryContract {
- override suspend fun save(fileId: FileId, fileType: FileType, input: Flow, creationTime: UnixTime) {
- postgresqlFilesDataSource.createFile(
- fileId = fileId.string,
- fileName = fileId.string,
- fileType = mapper.domainTypeToDbType(fileType),
- filePath = localFilesDataSource.localFilesPath.pathString,
- creationTime = creationTime.inMilliseconds,
- )
-
- localFilesDataSource.save(fileId.string, mapper.domainTypeToLocalFilesType(fileType), input)
- }
-
- override suspend fun retrieve(file: File): InputStream? {
- return localFilesDataSource.retrieve(file.fileId.string, LocalFilesDataSource.FileType.IMAGE)
- }
-
- override suspend fun remove(fileId: FileId) {
- localFilesDataSource.remove(fileId.string, LocalFilesDataSource.FileType.IMAGE)
- }
-
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/files/datasource/FileEntityMapper.kt b/data/src/main/kotlin/io/timemates/backend/data/files/datasource/FileEntityMapper.kt
deleted file mode 100644
index 29ac96dc..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/files/datasource/FileEntityMapper.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-package io.timemates.backend.data.files.datasource
-
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.data.common.markers.Mapper
-import io.timemates.backend.data.files.datasource.PostgresqlFilesDataSource.FileType
-import io.timemates.backend.files.types.value.FileId
-import org.jetbrains.exposed.sql.ResultRow
-import io.timemates.backend.files.types.File as DomainFile
-import io.timemates.backend.files.types.FileType as DomainFileType
-
-
-class FileEntityMapper : Mapper {
- fun toDomainFile(file: PostgresqlFilesDataSource.File): DomainFile {
- return when (file.fileType) {
- FileType.IMAGE -> DomainFile.Image(
- FileId.createOrThrowInternally(file.fileId),
- )
- }
- }
-
- fun domainTypeToLocalFilesType(fileType: DomainFileType): LocalFilesDataSource.FileType {
- return when (fileType) {
- DomainFileType.IMAGE -> LocalFilesDataSource.FileType.IMAGE
- }
- }
-
- fun domainTypeToDbType(fileType: DomainFileType): FileType {
- return when (fileType) {
- DomainFileType.IMAGE -> FileType.IMAGE
- }
- }
-
- fun resultRowToPSqlFile(resultRow: ResultRow): PostgresqlFilesDataSource.File = with(resultRow) {
- return@with PostgresqlFilesDataSource.File(
- get(PostgresqlFilesDataSource.FilesTable.FILE_ID),
- get(PostgresqlFilesDataSource.FilesTable.FILE_NAME),
- get(PostgresqlFilesDataSource.FilesTable.FILE_TYPE),
- get(PostgresqlFilesDataSource.FilesTable.FILE_PATH),
- get(PostgresqlFilesDataSource.FilesTable.CREATION_TIME),
- )
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/files/datasource/LocalFilesDataSource.kt b/data/src/main/kotlin/io/timemates/backend/data/files/datasource/LocalFilesDataSource.kt
deleted file mode 100644
index f2ef8cad..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/files/datasource/LocalFilesDataSource.kt
+++ /dev/null
@@ -1,52 +0,0 @@
-@file:Suppress("BlockingMethodInNonBlockingContext")
-
-package io.timemates.backend.data.files.datasource
-
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.withContext
-import java.io.InputStream
-import java.nio.file.Path
-import kotlin.io.path.*
-
-class LocalFilesDataSource(internal val localFilesPath: Path) {
-
- enum class FileType {
- IMAGE,
- }
-
- suspend fun save(fileId: String, fileType: FileType, inputStream: Flow) {
- withContext(Dispatchers.IO) {
- localFilesPath
- .fileSystem
- .getPath(fileType.name.lowercase())
- .fileSystem
- .getPath(fileId)
- .createFile()
- .outputStream()
- .use { stream ->
- inputStream.collect { bytes -> stream.write(bytes) }
- }
- }
- }
-
- suspend fun retrieve(fileId: String, fileType: FileType): InputStream? {
- return withContext(Dispatchers.IO) {
- localFilesPath.fileSystem
- .getPath(fileType.name.lowercase())
- .fileSystem
- .getPath(fileId)
- .takeIf { it.exists() }
- ?.inputStream()
- }
- }
-
- suspend fun remove(fileId: String, fileType: FileType) {
- return withContext(Dispatchers.IO) {
- localFilesPath
- .fileSystem.getPath(fileType.name.lowercase())
- .fileSystem.getPath(fileId)
- .deleteIfExists()
- }
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/files/datasource/PostgresqlFilesDataSource.kt b/data/src/main/kotlin/io/timemates/backend/data/files/datasource/PostgresqlFilesDataSource.kt
deleted file mode 100644
index 4403e398..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/files/datasource/PostgresqlFilesDataSource.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-package io.timemates.backend.data.files.datasource
-
-import io.timemates.backend.data.files.datasource.PostgresqlFilesDataSource.FilesTable.FILE_ID
-import io.timemates.backend.exposed.suspendedTransaction
-import org.jetbrains.annotations.TestOnly
-import org.jetbrains.exposed.sql.*
-import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
-import org.jetbrains.exposed.sql.transactions.transaction
-
-class PostgresqlFilesDataSource(private val database: Database, private val mapper: FileEntityMapper) {
- internal object FilesTable : Table("files") {
- val FILE_ID = text("file_id")
- val FILE_NAME = text("file_name")
- val FILE_TYPE = enumeration("file_type")
- val CREATION_TIME = long("creation_time")
- val FILE_PATH = text("file_path")
-
- override val primaryKey: PrimaryKey = PrimaryKey(FILE_ID)
- }
-
- init {
- transaction(database) {
- SchemaUtils.create(FilesTable)
- }
- }
-
- suspend fun isFileExists(id: String): Boolean = suspendedTransaction(database) {
- FilesTable.select { FILE_ID eq id }.any()
- }
-
- suspend fun getFile(id: String): File? = suspendedTransaction(database) {
- FilesTable.select { FILE_ID eq id }.singleOrNull()?.let(mapper::resultRowToPSqlFile)
- }
-
- suspend fun createFile(fileId: String, fileName: String, fileType: FileType, filePath: String, creationTime: Long) =
- suspendedTransaction(database) {
- FilesTable.insert {
- it[FILE_ID] = fileId
- it[FILE_NAME] = fileName
- it[CREATION_TIME] = creationTime
- it[FILE_TYPE] = fileType
- it[FILE_PATH] = filePath
- }[FILE_ID]
- }
-
- suspend fun deleteFile(fileId: String) =
- suspendedTransaction(database) {
- FilesTable.deleteWhere {
- FILE_ID eq fileId
- }
- }
-
- data class File(
- val fileId: String,
- val fileName: String,
- val fileType: FileType,
- val filePath: String,
- val fileCreationTime: Long,
- )
-
- enum class FileType {
- IMAGE,
- }
-
- @TestOnly
- suspend fun clear() = suspendedTransaction(database) {
- FilesTable.deleteAll()
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/CoPostgresqlTimerSessionRepository.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/CoPostgresqlTimerSessionRepository.kt
deleted file mode 100644
index 32d8ef39..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/CoPostgresqlTimerSessionRepository.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package io.timemates.backend.data.timers
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.data.timers.db.PostgresqlStateStorageRepository
-import io.timemates.backend.data.timers.db.TableTimersSessionUsersDataSource
-import io.timemates.backend.data.timers.db.TableTimersStateDataSource
-import io.timemates.backend.data.timers.mappers.TimerSessionMapper
-import io.timemates.backend.fsm.CoroutinesStateMachine
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.pagination.map
-import io.timemates.backend.timers.fsm.InactiveState
-import io.timemates.backend.timers.fsm.TimerState
-import io.timemates.backend.timers.fsm.TimersStateMachine
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.UserId
-import io.timemates.backend.validation.createOrThrowInternally
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.SupervisorJob
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
-
-class CoPostgresqlTimerSessionRepository(
- coroutineScope: CoroutineScope =
- CoroutineScope(Dispatchers.Default + SupervisorJob()),
- private val tableTimersSessionUsers: TableTimersSessionUsersDataSource,
- private val tableTimersStateDataSource: TableTimersStateDataSource,
- private val sessionsMapper: TimerSessionMapper,
- private val timeProvider: TimeProvider,
- private val timersRepository: TimersRepository,
-) : TimerSessionRepository, TimersStateMachine {
- private val coStateMachine: TimersStateMachine =
- CoroutinesStateMachine(
- coroutineScope = coroutineScope,
- storage = PostgresqlStateStorageRepository(
- tableTimersStateDataSource = tableTimersStateDataSource,
- sessionsMapper = sessionsMapper,
- timeProvider = timeProvider,
- timersRepository = timersRepository,
- timersSessionRepository = this
- ),
- timeProvider = timeProvider,
- )
-
- override suspend fun addUser(timerId: TimerId, userId: UserId, joinTime: UnixTime) {
- tableTimersSessionUsers.assignUser(
- timerId.long,
- userId.long,
- true,
- joinTime.inMilliseconds,
- )
- }
-
- override suspend fun removeUser(timerId: TimerId, userId: UserId) {
- tableTimersSessionUsers.unassignUser(timerId.long, userId.long)
- }
-
- override suspend fun getTimerIdOfCurrentSession(userId: UserId, lastActiveTime: UnixTime): TimerId? {
- return tableTimersSessionUsers.getTimerIdFromUserSession(userId.long, lastActiveTime.inMilliseconds)
- ?.let { TimerId.createOrThrowInternally(it) }
- }
-
- override suspend fun getMembers(
- timerId: TimerId,
- pageToken: PageToken?,
- lastActiveTime: UnixTime,
- ): Page {
- return tableTimersSessionUsers.getUsers(
- timerId.long,
- pageToken,
- lastActiveTime.inMilliseconds,
- ).map { sessionUser -> UserId.createOrThrowInternally(sessionUser.userId) }
- }
-
- override suspend fun getMembersCount(timerId: TimerId, activeAfterTime: UnixTime): Count {
- return tableTimersSessionUsers.getUsersCount(timerId.long, activeAfterTime.inMilliseconds)
- .let { Count.createOrThrowInternally(it) }
- }
-
- override suspend fun setActiveUsersConfirmationRequirement(timerId: TimerId) {
- tableTimersSessionUsers.setAllAsNotConfirmed(timerId.long)
- }
-
- override suspend fun markConfirmed(
- timerId: TimerId,
- userId: UserId,
- confirmationTime: UnixTime,
- ): Boolean {
- tableTimersSessionUsers.assignUser(
- timerId.long,
- userId.long,
- true,
- confirmationTime.inMilliseconds
- )
-
- return true
- }
-
- override suspend fun removeInactiveUsers(afterTime: UnixTime) {
- tableTimersSessionUsers.removeUsersBefore(afterTime.inMilliseconds)
- }
-
- override suspend fun removeNotConfirmedUsers(timerId: TimerId) {
- tableTimersSessionUsers.removeNotConfirmedUsers(timerId.long)
- }
-
- override suspend fun updateLastActivityTime(timerId: TimerId, userId: UserId, time: UnixTime) {
- tableTimersSessionUsers.updateLastActivityTime(timerId.long, userId.long, timerId.long)
- }
-
- override suspend fun setState(key: TimerId, state: TimerState) {
- coStateMachine.setState(key, state)
- }
-
- override suspend fun sendEvent(key: TimerId, event: TimerEvent): Boolean {
- return coStateMachine.sendEvent(key, event)
- }
-
- override suspend fun getState(key: TimerId): Flow {
- return coStateMachine.getState(key) ?: flowOf(
- tableTimersStateDataSource.getTimerState(key.long)
- ?.let {
- sessionsMapper.dbStateToFsmState(
- it, timeProvider, timersRepository, this
- )
- } ?: InactiveState(key, UnixTime.ZERO, timersRepository, this, timeProvider),
- )
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/cache/CacheTimersSessionUsersDataSource.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/cache/CacheTimersSessionUsersDataSource.kt
deleted file mode 100644
index 6da74c6a..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/cache/CacheTimersSessionUsersDataSource.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-package io.timemates.backend.data.timers.cache
-
-import io.github.reactivecircus.cache4k.Cache
-import io.timemates.backend.data.timers.cache.entities.CacheSessionUser
-
-class CacheTimersSessionUsersDataSource(maxEntities: Long) {
- private val cache = Cache.Builder()
- .maximumCacheSize(maxEntities)
- .build()
-
- fun getOrNull(id: Long): CacheSessionUser? = cache.get(id)
- fun save(id: Long, cacheSessionUser: CacheSessionUser): Unit = cache.put(id, cacheSessionUser)
- fun remove(id: Long): Unit = cache.invalidate(id)
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/PostgresqlStateStorageRepository.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/db/PostgresqlStateStorageRepository.kt
deleted file mode 100644
index 34583b3b..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/PostgresqlStateStorageRepository.kt
+++ /dev/null
@@ -1,38 +0,0 @@
-package io.timemates.backend.data.timers.db
-
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.data.timers.mappers.TimerSessionMapper
-import io.timemates.backend.fsm.StateStorage
-import io.timemates.backend.timers.fsm.TimerState
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerEvent
-import io.timemates.backend.timers.types.value.TimerId
-
-class PostgresqlStateStorageRepository(
- private val tableTimersStateDataSource: TableTimersStateDataSource,
- private val sessionsMapper: TimerSessionMapper,
- private val timeProvider: TimeProvider,
- private val timersRepository: TimersRepository,
- private val timersSessionRepository: TimerSessionRepository,
-) : StateStorage {
- override suspend fun save(key: TimerId, state: TimerState) {
- tableTimersStateDataSource.setTimerState(sessionsMapper.fsmStateToDbState(state))
- }
-
- override suspend fun remove(key: TimerId): Boolean {
- return tableTimersStateDataSource.removeTimerState(key.long)
- }
-
- override suspend fun load(key: TimerId): TimerState? {
- return tableTimersStateDataSource.getTimerState(key.long)
- ?.let {
- sessionsMapper.dbStateToFsmState(
- dbState = it,
- timeProvider = timeProvider,
- timersRepository = timersRepository,
- timersSessionRepository = timersSessionRepository,
- )
- }
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimerInvitesMapper.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimerInvitesMapper.kt
deleted file mode 100644
index 214df639..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimerInvitesMapper.kt
+++ /dev/null
@@ -1,32 +0,0 @@
-package io.timemates.backend.data.timers.mappers
-
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.data.common.markers.Mapper
-import io.timemates.backend.data.timers.db.entities.DbInvite
-import io.timemates.backend.data.timers.db.tables.TimersInvitesTable
-import io.timemates.backend.timers.types.Invite
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.timers.types.value.TimerId
-import org.jetbrains.exposed.sql.ResultRow
-
-class TimerInvitesMapper : Mapper {
- fun resultRowToDbInvite(resultRow: ResultRow): DbInvite = with(resultRow) {
- return DbInvite(
- timerId = get(TimersInvitesTable.TIMER_ID),
- maxJoiners = get(TimersInvitesTable.MAX_JOINERS_COUNT),
- inviteCode = get(TimersInvitesTable.INVITE_CODE),
- creationTime = get(TimersInvitesTable.CREATION_TIME),
- )
- }
-
- fun dbInviteToDomainInvite(dbInvite: DbInvite): Invite = with(dbInvite) {
- return Invite(
- timerId = TimerId.createOrThrowInternally(timerId),
- code = InviteCode.createOrThrowInternally(inviteCode),
- creationTime = UnixTime.createOrThrowInternally(creationTime),
- limit = Count.createOrThrowInternally(maxJoiners),
- )
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimerSessionMapper.kt b/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimerSessionMapper.kt
deleted file mode 100644
index d1dfcfd9..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimerSessionMapper.kt
+++ /dev/null
@@ -1,104 +0,0 @@
-package io.timemates.backend.data.timers.mappers
-
-import com.timemates.backend.time.TimeProvider
-import com.timemates.backend.time.UnixTime
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.data.common.markers.Mapper
-import io.timemates.backend.data.timers.db.entities.DbSessionUser
-import io.timemates.backend.data.timers.db.entities.DbTimer
-import io.timemates.backend.data.timers.db.tables.TimersSessionUsersTable
-import io.timemates.backend.timers.fsm.*
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.value.TimerId
-import org.jetbrains.exposed.sql.ResultRow
-import kotlin.time.Duration.Companion.milliseconds
-
-class TimerSessionMapper : Mapper {
- fun resultRowToSessionUser(resultRow: ResultRow): DbSessionUser = with(resultRow) {
- return DbSessionUser(
- timerId = get(TimersSessionUsersTable.TIMER_ID),
- userId = get(TimersSessionUsersTable.USER_ID),
- lastActivityTime = get(TimersSessionUsersTable.LAST_ACTIVITY_TIME),
- )
- }
-
- fun dbStateToFsmState(
- dbState: DbTimer.State,
- timeProvider: TimeProvider,
- timersRepository: TimersRepository,
- timersSessionRepository: TimerSessionRepository,
- ): TimerState = with(dbState) {
- val timerId = TimerId.createOrThrowInternally(timerId)
- val publishTime = UnixTime.createOrThrowInternally(creationTime)
- val alive = (endsAt?.let { (creationTime - it) } ?: Long.MAX_VALUE).milliseconds
-
- val state = when (phase) {
- DbTimer.State.Phase.ATTENDANCE_CONFIRMATION -> ConfirmationState(
- timerId = timerId,
- timeProvider = timeProvider,
- timersRepository = timersRepository,
- publishTime = publishTime,
- alive = alive,
- timerSessionRepository = timersSessionRepository,
- )
-
- DbTimer.State.Phase.OFFLINE -> InactiveState(
- timerId = timerId,
- publishTime = publishTime,
- timerSessionRepository = timersSessionRepository,
- timeProvider = timeProvider,
- timersRepository = timersRepository,
- )
-
- DbTimer.State.Phase.PAUSED ->
- PauseState(
- timerId = timerId,
- publishTime = UnixTime.createOrThrowInternally(creationTime),
- timersRepository = timersRepository,
- timeProvider = timeProvider,
- timerSessionRepository = timersSessionRepository,
- )
-
- DbTimer.State.Phase.REST ->
- RestState(
- timerId = timerId,
- timersRepository = timersRepository,
- timeProvider = timeProvider,
- timerSessionRepository = timersSessionRepository,
- alive = alive,
- publishTime = publishTime,
- )
-
- DbTimer.State.Phase.RUNNING ->
- RunningState(
- timerId = timerId,
- timersRepository = timersRepository,
- timeProvider = timeProvider,
- timerSessionRepository = timersSessionRepository,
- alive = alive,
- publishTime = publishTime,
- )
- }
-
- return@with state
- }
-
-
- fun fsmStateToDbState(state: TimerState): DbTimer.State = with(state) {
- val phase = when (state) {
- is ConfirmationState -> DbTimer.State.Phase.ATTENDANCE_CONFIRMATION
- is InactiveState -> DbTimer.State.Phase.OFFLINE
- is PauseState -> DbTimer.State.Phase.PAUSED
- is RestState -> DbTimer.State.Phase.REST
- is RunningState -> DbTimer.State.Phase.RUNNING
- }
-
- return@with DbTimer.State(
- timerId = timerId.long,
- phase = phase,
- endsAt = (publishTime + alive).inMilliseconds,
- creationTime = publishTime.inMilliseconds
- )
- }
-}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/users/UserEntitiesMapper.kt b/data/src/main/kotlin/io/timemates/backend/data/users/UserEntitiesMapper.kt
deleted file mode 100644
index 57dec226..00000000
--- a/data/src/main/kotlin/io/timemates/backend/data/users/UserEntitiesMapper.kt
+++ /dev/null
@@ -1,59 +0,0 @@
-package io.timemates.backend.data.users
-
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.data.common.markers.Mapper
-import io.timemates.backend.data.users.datasource.CachedUsersDataSource
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource
-import io.timemates.backend.users.types.Avatar
-import io.timemates.backend.users.types.User
-import io.timemates.backend.users.types.value.EmailAddress
-import io.timemates.backend.users.types.value.UserDescription
-import io.timemates.backend.users.types.value.UserId
-import io.timemates.backend.users.types.value.UserName
-
-class UserEntitiesMapper : Mapper {
- fun toDomainUser(id: Long, cachedUser: CachedUsersDataSource.User): User = with(cachedUser) {
- return User(
- UserId.createOrThrowInternally(id),
- UserName.createOrThrowInternally(name),
- email?.let { EmailAddress.createOrThrowInternally(it) },
- shortBio?.let { UserDescription.createOrThrowInternally(it) },
- avatar = avatarFileId?.let { Avatar.FileId.createOrThrowInternally(it) }
- ?: gravatarId?.let { Avatar.GravatarId.createOrThrowInternally(it) }
- )
- }
-
- fun toCachedUser(pUser: PostgresqlUsersDataSource.User): CachedUsersDataSource.User = with(pUser) {
- return CachedUsersDataSource.User(userName, userShortDesc, userAvatarFileId, userGravatarId, userEmail)
- }
-
- fun toPostgresqlUserPatch(patch: User.Patch) = with(patch) {
- PostgresqlUsersDataSource.User.Patch(
- userName = name?.string,
- userShortDesc = description?.string,
- userAvatarFileId = (avatar as? Avatar.FileId)?.string,
- userGravatarId = (avatar as? Avatar.GravatarId)?.string
- )
- }
-
- fun toCachedUser(user: User) = with(user) {
- CachedUsersDataSource.User(
- name.string,
- description?.string,
- avatarFileId = (avatar as? Avatar.FileId)?.string,
- gravatarId = (avatar as? Avatar.GravatarId)?.string,
- emailAddress?.string
- )
- }
-
- fun toDomainUser(pUser: PostgresqlUsersDataSource.User): User = with(pUser) {
- return User(
- UserId.createOrThrowInternally(id),
- UserName.createOrThrowInternally(userName),
- EmailAddress.createOrThrowInternally(userEmail),
- userShortDesc?.let { UserDescription.createOrThrowInternally(it) },
- avatar = pUser.userAvatarFileId?.let { Avatar.FileId.createOrThrowInternally(it) }
- ?: pUser.userGravatarId?.let { Avatar.GravatarId.createOrThrowInternally(it) }
- )
- }
-}
\ No newline at end of file
diff --git a/data/src/test/kotlin/io/timemates/backend/data/files/FileEntityMapperTest.kt b/data/src/test/kotlin/io/timemates/backend/data/files/FileEntityMapperTest.kt
deleted file mode 100644
index 1d99d9e1..00000000
--- a/data/src/test/kotlin/io/timemates/backend/data/files/FileEntityMapperTest.kt
+++ /dev/null
@@ -1,45 +0,0 @@
-package io.timemates.backend.data.files
-
-import com.timemates.random.SecureRandomProvider
-import io.timemates.backend.data.files.datasource.FileEntityMapper
-import io.timemates.backend.data.files.datasource.PostgresqlFilesDataSource
-import io.timemates.backend.files.types.File
-import io.timemates.backend.files.types.value.FileId
-import io.timemates.backend.testing.validation.createOrAssert
-import kotlin.test.Test
-import kotlin.test.assertEquals
-
-class FileEntityMapperTest {
- private val random = SecureRandomProvider()
- private val mapper = FileEntityMapper()
-
- @Test
- fun `toDomainMapper should map to domain file correctly`() {
- // GIVEN
- val fileId = random.randomHash(64)
- val fileName = "Default Name"
- val fileType = PostgresqlFilesDataSource.FileType.IMAGE
- val filePath = "C\\123"
- val fileCreationType = 4444L
-
- val expectedFile = File.Image(
- FileId.createOrAssert(fileId)
- )
-
- // WHEN
- val actualFile = mapper.toDomainFile(
- PostgresqlFilesDataSource.File(
- fileId,
- fileName,
- fileType, filePath,
- fileCreationType,
- )
- )
-
- // THEN
- assertEquals(
- expected = expectedFile,
- actual = actualFile
- )
- }
-}
\ No newline at end of file
diff --git a/data/src/test/kotlin/io/timemates/backend/data/files/datasource/PostgresqlFilesDataSourceTest.kt b/data/src/test/kotlin/io/timemates/backend/data/files/datasource/PostgresqlFilesDataSourceTest.kt
deleted file mode 100644
index 284e3b30..00000000
--- a/data/src/test/kotlin/io/timemates/backend/data/files/datasource/PostgresqlFilesDataSourceTest.kt
+++ /dev/null
@@ -1,67 +0,0 @@
-package io.timemates.backend.data.files.datasource
-
-import com.timemates.random.SecureRandomProvider
-import io.timemates.backend.files.types.value.FileId
-import junit.framework.TestCase.assertNull
-import kotlinx.coroutines.runBlocking
-import org.jetbrains.exposed.sql.Database
-import kotlin.test.*
-
-class PostgresqlFilesDataSourceTest {
- private val random = SecureRandomProvider()
- private val databaseUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;"
- private val databaseDriver = "org.h2.Driver"
-
- private val fileCreationTime = 4444L
- private val fileId = random.randomHash(FileId.SIZE)
- private val fileName = "DefaultFile"
- private val filePath = "C\\123"
- private val fileType = PostgresqlFilesDataSource.FileType.IMAGE
-
- private val database = Database.connect(databaseUrl, databaseDriver)
- private val mapper = FileEntityMapper()
- private val datasource = PostgresqlFilesDataSource(database, mapper)
-
- @BeforeTest
- fun beforeEach(): Unit = runBlocking {
- datasource.clear()
- }
-
- @Test
- fun `isFileExists should return true if file exists`(): Unit = runBlocking {
- // WHEN
- val fileId = datasource.createFile(fileId, fileName, fileType, filePath, fileCreationTime)
- val result = datasource.isFileExists(fileId)
- assertTrue(result)
- }
-
- @Test
- fun `isFileExists should return false if file does not exist`() {
- val result = runBlocking { datasource.isFileExists(fileId) }
- assertFalse(result)
- }
-
- @Test
- fun `getFile should return file if file with id exists`() = runBlocking {
- val fileId = datasource.createFile(fileId, fileName, fileType, filePath, fileCreationTime)
- val expectedFile = PostgresqlFilesDataSource.File(
- fileId = fileId,
- fileName = fileName,
- fileType = fileType,
- filePath = filePath,
- fileCreationTime = fileCreationTime,
- )
-
- val result = datasource.getFile(fileId)
- assertEquals(
- expected = expectedFile,
- actual = result
- )
- }
-
- @Test
- fun `getFile should return null if file with id does not exist`() {
- val result = runBlocking { datasource.getFile(fileId) }
- assertNull(result)
- }
-}
\ No newline at end of file
diff --git a/features/auth/adapters/build.gradle.kts b/features/auth/adapters/build.gradle.kts
new file mode 100644
index 00000000..3042a1bf
--- /dev/null
+++ b/features/auth/adapters/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+dependencies {
+ implementation(projects.features.auth.domain)
+ implementation(projects.features.users.domain)
+
+ implementation(projects.foundation.random)
+ implementation(projects.foundation.smtpMailer)
+ implementation(libs.ktor.client.content.negotiation)
+ implementation(libs.ktor.client.logging)
+ implementation(libs.ktor.json)
+
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.ktor.client.core)
+ implementation(libs.ktor.client.cio)
+}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/common/repositories/MailerSendEmailsRepository.kt b/features/auth/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/MailerSendEmailRepositoryAdapter.kt
similarity index 63%
rename from data/src/main/kotlin/io/timemates/backend/data/common/repositories/MailerSendEmailsRepository.kt
rename to features/auth/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/MailerSendEmailRepositoryAdapter.kt
index aa0590b8..31712db9 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/common/repositories/MailerSendEmailsRepository.kt
+++ b/features/auth/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/MailerSendEmailRepositoryAdapter.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.common.repositories
+package org.timemates.backend.auth.adapters
import io.ktor.client.*
import io.ktor.client.engine.cio.*
@@ -8,16 +8,17 @@ import io.ktor.client.plugins.logging.*
import io.ktor.client.request.*
import io.ktor.http.*
import io.ktor.serialization.kotlinx.json.*
-import io.timemates.backend.authorization.types.Email
-import io.timemates.backend.common.repositories.EmailsRepository
-import io.timemates.backend.users.types.value.EmailAddress
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
+import org.timemates.backend.auth.domain.repositories.EmailRepository
+import org.timemates.backend.types.auth.Email
+import org.timemates.backend.types.users.value.EmailAddress
-class MailerSendEmailsRepository(
+class MailerSendEmailRepositoryAdapter(
private val configuration: Configuration,
client: HttpClient = HttpClient(CIO),
-) : EmailsRepository {
+ private val isDebug: Boolean = false,
+) : EmailRepository {
data class Configuration(
val apiKey: String,
val sender: String,
@@ -37,10 +38,13 @@ class MailerSendEmailsRepository(
json()
}
- install(Logging) {
- logger = Logger.DEFAULT
- level = LogLevel.ALL
- }
+ developmentMode = isDebug
+
+ if (isDebug)
+ install(Logging) {
+ logger = Logger.DEFAULT
+ level = LogLevel.ALL
+ }
}
override suspend fun send(emailAddress: EmailAddress, email: Email): Boolean = try {
@@ -68,27 +72,27 @@ class MailerSendEmailsRepository(
e.printStackTrace()
false
}
-}
-@Serializable
-internal data class EmailRequest(
- val subject: String,
- val from: From,
- val to: List,
- val variables: List,
- @SerialName("template_id")
- val templateId: String,
-)
+ @Serializable
+ private data class EmailRequest(
+ val subject: String,
+ val from: From,
+ val to: List,
+ val variables: List,
+ @SerialName("template_id")
+ val templateId: String,
+ )
-@Serializable
-internal data class From(val email: String)
+ @Serializable
+ private data class From(val email: String)
-@Serializable
-internal data class To(val email: String)
+ @Serializable
+ private data class To(val email: String)
-@Serializable
-internal data class Variable(val email: String, val substitutions: List)
+ @Serializable
+ private data class Variable(val email: String, val substitutions: List)
-@Serializable
-internal data class Substitution(@SerialName("var") val varName: String, val value: String)
\ No newline at end of file
+ @Serializable
+ private data class Substitution(@SerialName("var") val varName: String, val value: String)
+}
\ No newline at end of file
diff --git a/features/auth/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/UsersRepositoryDelegateAdapter.kt b/features/auth/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/UsersRepositoryDelegateAdapter.kt
new file mode 100644
index 00000000..ea4862db
--- /dev/null
+++ b/features/auth/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/UsersRepositoryDelegateAdapter.kt
@@ -0,0 +1,25 @@
+package org.timemates.backend.auth.adapters
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.auth.domain.repositories.UsersRepository
+import org.timemates.backend.types.users.value.EmailAddress
+import org.timemates.backend.types.users.value.UserDescription
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.types.users.value.UserName
+import org.timemates.backend.users.domain.repositories.UsersRepository as UsersRepositoryDelegate
+
+class UsersRepositoryDelegateAdapter(private val delegate: UsersRepositoryDelegate) : UsersRepository {
+ override suspend fun create(
+ emailAddress: EmailAddress,
+ userName: UserName,
+ userDescription: UserDescription?,
+ creationTime: UnixTime,
+ ): Result = runCatching {
+ delegate.createUser(emailAddress, userName, userDescription, creationTime)
+ }
+
+ override suspend fun get(emailAddress: EmailAddress): Result = runCatching {
+ delegate.getUserIdByEmail(emailAddress)
+ }
+
+}
\ No newline at end of file
diff --git a/features/auth/data/build.gradle.kts b/features/auth/data/build.gradle.kts
new file mode 100644
index 00000000..52e608d9
--- /dev/null
+++ b/features/auth/data/build.gradle.kts
@@ -0,0 +1,28 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+tasks.withType {
+ useJUnitPlatform()
+}
+
+dependencies {
+ implementation(projects.features.auth.domain)
+ implementation(projects.foundation.exposedUtils)
+ implementation(projects.foundation.pageToken)
+ implementation(projects.foundation.hashing)
+
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.exposed.core)
+ implementation(libs.cache4k)
+
+ testImplementation(libs.mockk)
+ testImplementation(libs.kotlin.test)
+ testImplementation(libs.kotlinx.coroutines.test)
+
+ testImplementation(libs.exposed.jdbc)
+ testImplementation(libs.h2.database)
+
+ testImplementation(projects.foundation.validation.testsIntegration)
+}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlAuthorizationsRepository.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/PostgresqlAuthorizationsRepository.kt
similarity index 73%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlAuthorizationsRepository.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/PostgresqlAuthorizationsRepository.kt
index d090aa5e..ba82adcd 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/PostgresqlAuthorizationsRepository.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/PostgresqlAuthorizationsRepository.kt
@@ -1,23 +1,25 @@
-package io.timemates.backend.data.authorization
+package org.timemates.backend.auth.data
import com.timemates.backend.time.UnixTime
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.AuthorizationId
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.data.authorization.cache.CacheAuthorizationsDataSource
-import io.timemates.backend.data.authorization.cache.entities.CacheAuthorization
-import io.timemates.backend.data.authorization.db.TableAuthorizationsDataSource
-import io.timemates.backend.data.authorization.db.entities.DbAuthorization
-import io.timemates.backend.data.authorization.mapper.AuthorizationsMapper
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.pagination.map
-import io.timemates.backend.users.types.value.UserId
-import io.timemates.backend.validation.createOrThrowInternally
+import org.timemates.backend.auth.data.cache.CacheAuthorizationsDataSource
+import org.timemates.backend.auth.data.cache.entities.CacheAuthorization
+import org.timemates.backend.auth.data.db.TableAuthorizationsDataSource
+import org.timemates.backend.auth.data.db.entities.DbAuthorization
+import org.timemates.backend.auth.data.mapper.AuthorizationsMapper
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.pagination.map
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.AuthorizationId
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+@OptIn(ValidationDelicateApi::class)
class PostgresqlAuthorizationsRepository(
private val tableAuthorizationsDataSource: TableAuthorizationsDataSource,
private val cacheAuthorizations: CacheAuthorizationsDataSource,
@@ -31,7 +33,7 @@ class PostgresqlAuthorizationsRepository(
creationTime: UnixTime,
clientMetadata: ClientMetadata,
): AuthorizationId {
- val id = tableAuthorizationsDataSource.createAuthorizations(
+ val id = tableAuthorizationsDataSource.createAuthorization(
userId = userId.long,
accessHash = accessToken.string,
refreshAccessHash = refreshToken.string,
@@ -60,7 +62,7 @@ class PostgresqlAuthorizationsRepository(
)
)
- return AuthorizationId.createOrThrowInternally(id)
+ return AuthorizationId.createUnsafe(id)
}
override suspend fun remove(accessToken: AccessHash): Boolean {
diff --git a/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/PostgresqlVerificationsRepository.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/PostgresqlVerificationsRepository.kt
new file mode 100644
index 00000000..9e290ec8
--- /dev/null
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/PostgresqlVerificationsRepository.kt
@@ -0,0 +1,68 @@
+package org.timemates.backend.auth.data
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.auth.data.db.TableVerificationsDataSource
+import org.timemates.backend.auth.data.mapper.VerificationsMapper
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.types.auth.Verification
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.value.Attempts
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.auth.value.VerificationHash
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.users.value.EmailAddress
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+
+class PostgresqlVerificationsRepository(
+ private val dbVerifications: TableVerificationsDataSource,
+ private val mapper: VerificationsMapper,
+) : VerificationsRepository {
+ override suspend fun save(
+ emailAddress: EmailAddress,
+ verificationToken: VerificationHash,
+ code: VerificationCode,
+ time: UnixTime,
+ attempts: Attempts,
+ clientMetadata: ClientMetadata,
+ ): Result = runCatching {
+ dbVerifications.add(
+ emailAddress.string,
+ verificationToken.string,
+ code.string,
+ time.inMilliseconds,
+ attempts.int,
+ clientMetadata.clientName.string,
+ clientMetadata.clientVersion.double,
+ clientMetadata.clientIpAddress.string
+ )
+ }
+
+ override suspend fun addAttempt(verificationToken: VerificationHash): Result = runCatching {
+ dbVerifications.decreaseAttempts(verificationToken.string)
+ }
+
+ override suspend fun getVerification(verificationToken: VerificationHash): Result = runCatching {
+ dbVerifications.getVerification(verificationToken.string)?.let(mapper::dbToDomain)
+ }
+
+ override suspend fun remove(verificationToken: VerificationHash): Result = runCatching {
+ dbVerifications.remove(verificationToken.string)
+ }
+
+ @OptIn(ValidationDelicateApi::class)
+ override suspend fun getNumberOfAttempts(emailAddress: EmailAddress, after: UnixTime): Result = runCatching {
+ dbVerifications.getAttempts(emailAddress.string, after.inMilliseconds)
+ .let { Count.createUnsafe(it) }
+ }
+
+ @OptIn(ValidationDelicateApi::class)
+ override suspend fun getNumberOfSessions(emailAddress: EmailAddress, after: UnixTime): Result = runCatching {
+ dbVerifications.getSessionsCount(emailAddress.string, after.inMilliseconds)
+ .let { Count.createUnsafe(it) }
+ }
+
+ override suspend fun markConfirmed(verificationToken: VerificationHash) {
+ dbVerifications.setAsConfirmed(verificationToken.string)
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/cache/CacheAuthorizationsDataSource.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/CacheAuthorizationsDataSource.kt
similarity index 78%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/cache/CacheAuthorizationsDataSource.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/CacheAuthorizationsDataSource.kt
index ef9788da..6fec61cb 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/cache/CacheAuthorizationsDataSource.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/CacheAuthorizationsDataSource.kt
@@ -1,7 +1,7 @@
-package io.timemates.backend.data.authorization.cache
+package org.timemates.backend.auth.data.cache
import io.github.reactivecircus.cache4k.Cache
-import io.timemates.backend.data.authorization.cache.entities.CacheAuthorization
+import org.timemates.backend.auth.data.cache.entities.CacheAuthorization
import kotlin.time.Duration
class CacheAuthorizationsDataSource(maxCacheEntities: Long, maxAliveTime: Duration) {
@@ -10,7 +10,7 @@ class CacheAuthorizationsDataSource(maxCacheEntities: Long, maxAliveTime: Durati
.expireAfterWrite(maxAliveTime)
.build()
- suspend fun getAuthorization(
+ fun getAuthorization(
accessHash: String,
): CacheAuthorization? {
return cache.get(accessHash)
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/cache/entities/CacheAuthorization.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/entities/CacheAuthorization.kt
similarity index 77%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/cache/entities/CacheAuthorization.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/entities/CacheAuthorization.kt
index 1473c7d4..75111f3f 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/cache/entities/CacheAuthorization.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/entities/CacheAuthorization.kt
@@ -1,6 +1,6 @@
-package io.timemates.backend.data.authorization.cache.entities
+package org.timemates.backend.auth.data.cache.entities
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
+import org.timemates.backend.types.auth.metadata.ClientMetadata
data class CacheAuthorization(
val userId: Long,
@@ -15,14 +15,12 @@ data class CacheAuthorization(
val authorization: GrantLevel,
val users: GrantLevel,
val timers: GrantLevel,
- val files: GrantLevel,
) {
companion object {
val All = Permissions(
authorization = GrantLevel.WRITE,
users = GrantLevel.WRITE,
timers = GrantLevel.WRITE,
- files = GrantLevel.WRITE,
)
}
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/cache/mapper/CacheAuthorizationsMapper.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/mapper/CacheAuthorizationsMapper.kt
similarity index 56%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/cache/mapper/CacheAuthorizationsMapper.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/mapper/CacheAuthorizationsMapper.kt
index 90c81f16..a42b0570 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/cache/mapper/CacheAuthorizationsMapper.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/cache/mapper/CacheAuthorizationsMapper.kt
@@ -1,15 +1,16 @@
-package io.timemates.backend.data.authorization.cache.mapper
+package org.timemates.backend.auth.data.cache.mapper
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.metadata.value.ClientIpAddress
-import io.timemates.backend.authorization.types.metadata.value.ClientName
-import io.timemates.backend.authorization.types.metadata.value.ClientVersion
-import io.timemates.backend.data.authorization.cache.entities.CacheAuthorization
-import io.timemates.backend.data.authorization.db.entities.DbAuthorization
-import io.timemates.backend.data.common.markers.Mapper
+import org.timemates.backend.auth.data.cache.entities.CacheAuthorization
+import org.timemates.backend.auth.data.db.entities.DbAuthorization
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.metadata.value.ClientIpAddress
+import org.timemates.backend.types.auth.metadata.value.ClientName
+import org.timemates.backend.types.auth.metadata.value.ClientVersion
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
-class CacheAuthorizationsMapper : Mapper {
+@OptIn(ValidationDelicateApi::class)
+class CacheAuthorizationsMapper {
fun dbToCacheAuthorization(auth: DbAuthorization): CacheAuthorization = with(auth) {
CacheAuthorization(
userId = userId,
@@ -19,9 +20,13 @@ class CacheAuthorizationsMapper : Mapper {
expiresAt = expiresAt,
createdAt = createdAt,
clientMetadata = ClientMetadata(
- clientName = ClientName.createOrThrowInternally(metaClientName),
- clientVersion = ClientVersion.createOrThrowInternally(metaClientVersion),
- clientIpAddress = ClientIpAddress.createOrThrowInternally(metaClientIpAddress),
+ clientName = ClientName.createUnsafe(metaClientName),
+ clientVersion = ClientVersion.createUnsafe(
+ metaClientVersion
+ ),
+ clientIpAddress = ClientIpAddress.createUnsafe(
+ metaClientIpAddress
+ ),
)
)
}
@@ -32,7 +37,6 @@ class CacheAuthorizationsMapper : Mapper {
CacheAuthorization.Permissions(
authorization = authorization.toCacheGrantLevel(),
users = users.toCacheGrantLevel(),
- files = files.toCacheGrantLevel(),
timers = timers.toCacheGrantLevel(),
)
}
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/TableAuthorizationsDataSource.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/TableAuthorizationsDataSource.kt
similarity index 79%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/TableAuthorizationsDataSource.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/TableAuthorizationsDataSource.kt
index ebf43b00..713ad583 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/TableAuthorizationsDataSource.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/TableAuthorizationsDataSource.kt
@@ -1,19 +1,21 @@
-package io.timemates.backend.data.authorization.db
+package org.timemates.backend.auth.data.db
-import io.timemates.backend.data.authorization.db.entities.AuthorizationPageToken
-import io.timemates.backend.data.authorization.db.entities.DbAuthorization
-import io.timemates.backend.data.authorization.db.mapper.DbAuthorizationsMapper
-import io.timemates.backend.data.authorization.db.table.AuthorizationsTable
-import io.timemates.backend.exposed.suspendedTransaction
-import io.timemates.backend.exposed.update
-import io.timemates.backend.pagination.Ordering
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
+import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
+import org.jetbrains.annotations.TestOnly
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
+import org.timemates.backend.auth.data.db.entities.AuthorizationPageToken
+import org.timemates.backend.auth.data.db.entities.DbAuthorization
+import org.timemates.backend.auth.data.db.mapper.DbAuthorizationsMapper
+import org.timemates.backend.auth.data.db.table.AuthorizationsTable
+import org.timemates.backend.exposed.suspendedTransaction
+import org.timemates.backend.exposed.update
+import org.timemates.backend.pagination.Ordering
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
class TableAuthorizationsDataSource(
private val database: Database,
@@ -35,12 +37,13 @@ class TableAuthorizationsDataSource(
AuthorizationsTable.AUTHORIZATION_ID greater (pageInfo?.lastReceivedAuthorizationId ?: 0))
}
.orderBy(AuthorizationsTable.AUTHORIZATION_ID, SortOrder.ASC)
+ .limit(20)
.map(mapper::resultRowToDbAuthorization)
val lastId = result.lastOrNull()?.authorizationId
val nextPageToken = if (lastId != null)
PageToken.toGive(json.encodeToString(AuthorizationPageToken(lastId)))
- else pageToken
+ else null
return@suspendedTransaction Page(
value = result,
@@ -63,7 +66,7 @@ class TableAuthorizationsDataSource(
AuthorizationsTable.deleteWhere { ACCESS_TOKEN eq accessHash } > 0
}
- suspend fun createAuthorizations(
+ suspend fun createAuthorization(
userId: Long,
accessHash: String,
refreshAccessHash: String,
@@ -85,7 +88,6 @@ class TableAuthorizationsDataSource(
it[META_CLIENT_IP_ADDRESS] = metaClientIpAddress
it[AUTHORIZATIONS_PERMISSION] = permissions.authorization
it[USERS_PERMISSION] = permissions.users
- it[FILES_PERMISSION] = permissions.files
it[TIMERS_PERMISSION] = permissions.timers
}[AuthorizationsTable.AUTHORIZATION_ID]
}
@@ -100,4 +102,9 @@ class TableAuthorizationsDataSource(
it[EXPIRES_AT] = expiresAt
} > 0
}
+
+ @TestOnly
+ suspend fun clearAll(): Unit = suspendedTransaction(database) {
+ AuthorizationsTable.deleteAll()
+ }
}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/TableVerificationsDataSource.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/TableVerificationsDataSource.kt
similarity index 81%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/TableVerificationsDataSource.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/TableVerificationsDataSource.kt
index f76f5e5e..5e5552f5 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/TableVerificationsDataSource.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/TableVerificationsDataSource.kt
@@ -1,15 +1,16 @@
-package io.timemates.backend.data.authorization.db
+package org.timemates.backend.auth.data.db
-import io.timemates.backend.data.authorization.db.entities.DbVerification
-import io.timemates.backend.data.authorization.db.mapper.DbVerificationsMapper
-import io.timemates.backend.data.authorization.db.table.VerificationSessionsTable
-import io.timemates.backend.data.authorization.db.table.VerificationSessionsTable.ATTEMPTS
-import io.timemates.backend.data.authorization.db.table.VerificationSessionsTable.VERIFICATION_HASH
-import io.timemates.backend.exposed.suspendedTransaction
-import io.timemates.backend.exposed.update
+import org.jetbrains.annotations.TestOnly
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
+import org.timemates.backend.auth.data.db.entities.DbVerification
+import org.timemates.backend.auth.data.db.mapper.DbVerificationsMapper
+import org.timemates.backend.auth.data.db.table.VerificationSessionsTable
+import org.timemates.backend.auth.data.db.table.VerificationSessionsTable.ATTEMPTS
+import org.timemates.backend.auth.data.db.table.VerificationSessionsTable.VERIFICATION_HASH
+import org.timemates.backend.exposed.suspendedTransaction
+import org.timemates.backend.exposed.update
class TableVerificationsDataSource(
private val database: Database,
@@ -93,4 +94,9 @@ class TableVerificationsDataSource(
suspend fun remove(verificationHash: String): Unit = suspendedTransaction(database) {
VerificationSessionsTable.deleteWhere { VERIFICATION_HASH eq verificationHash }
}
+
+ @TestOnly
+ suspend fun clearAll(): Unit = suspendedTransaction(database) {
+ VerificationSessionsTable.deleteAll()
+ }
}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/AuthorizationPageToken.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/AuthorizationPageToken.kt
similarity index 69%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/AuthorizationPageToken.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/AuthorizationPageToken.kt
index f8de1d18..6c1f0f1f 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/AuthorizationPageToken.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/AuthorizationPageToken.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.authorization.db.entities
+package org.timemates.backend.auth.data.db.entities
import kotlinx.serialization.Serializable
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/DbAuthorization.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/DbAuthorization.kt
similarity index 85%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/DbAuthorization.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/DbAuthorization.kt
index f068fa65..a8a77b12 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/DbAuthorization.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/DbAuthorization.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.authorization.db.entities
+package org.timemates.backend.auth.data.db.entities
data class DbAuthorization(
val authorizationId: Int,
@@ -16,14 +16,12 @@ data class DbAuthorization(
val authorization: GrantLevel,
val users: GrantLevel,
val timers: GrantLevel,
- val files: GrantLevel,
) {
companion object {
val All = Permissions(
authorization = GrantLevel.WRITE,
users = GrantLevel.WRITE,
timers = GrantLevel.WRITE,
- files = GrantLevel.WRITE,
)
}
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/DbVerification.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/DbVerification.kt
similarity index 82%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/DbVerification.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/DbVerification.kt
index e226678b..6d4485a4 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/entities/DbVerification.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/entities/DbVerification.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.authorization.db.entities
+package org.timemates.backend.auth.data.db.entities
data class DbVerification(
val verificationHash: String,
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/mapper/DbAuthorizationsMapper.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/mapper/DbAuthorizationsMapper.kt
similarity index 80%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/mapper/DbAuthorizationsMapper.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/mapper/DbAuthorizationsMapper.kt
index fbab0575..1068a300 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/mapper/DbAuthorizationsMapper.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/mapper/DbAuthorizationsMapper.kt
@@ -1,8 +1,8 @@
-package io.timemates.backend.data.authorization.db.mapper
+package org.timemates.backend.auth.data.db.mapper
-import io.timemates.backend.data.authorization.db.entities.DbAuthorization
-import io.timemates.backend.data.authorization.db.table.AuthorizationsTable
import org.jetbrains.exposed.sql.ResultRow
+import org.timemates.backend.auth.data.db.entities.DbAuthorization
+import org.timemates.backend.auth.data.db.table.AuthorizationsTable
class DbAuthorizationsMapper {
fun resultRowToDbAuthorization(row: ResultRow): DbAuthorization {
@@ -17,7 +17,6 @@ class DbAuthorizationsMapper {
authorization = row[AuthorizationsTable.AUTHORIZATIONS_PERMISSION],
users = row[AuthorizationsTable.USERS_PERMISSION],
timers = row[AuthorizationsTable.TIMERS_PERMISSION],
- files = row[AuthorizationsTable.FILES_PERMISSION],
),
metaClientName = row[AuthorizationsTable.META_CLIENT_NAME],
metaClientVersion = row[AuthorizationsTable.META_CLIENT_VERSION],
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/mapper/DbVerificationsMapper.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/mapper/DbVerificationsMapper.kt
similarity index 80%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/mapper/DbVerificationsMapper.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/mapper/DbVerificationsMapper.kt
index 5ed72897..9e1f5c35 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/mapper/DbVerificationsMapper.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/mapper/DbVerificationsMapper.kt
@@ -1,8 +1,8 @@
-package io.timemates.backend.data.authorization.db.mapper
+package org.timemates.backend.auth.data.db.mapper
-import io.timemates.backend.data.authorization.db.entities.DbVerification
-import io.timemates.backend.data.authorization.db.table.VerificationSessionsTable
import org.jetbrains.exposed.sql.ResultRow
+import org.timemates.backend.auth.data.db.entities.DbVerification
+import org.timemates.backend.auth.data.db.table.VerificationSessionsTable
class DbVerificationsMapper {
fun resultRowToDbVerification(resultRow: ResultRow): DbVerification = with(resultRow) {
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/table/AuthorizationsTable.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/table/AuthorizationsTable.kt
similarity index 67%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/table/AuthorizationsTable.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/table/AuthorizationsTable.kt
index 2f97b0df..ab9a6a64 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/table/AuthorizationsTable.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/table/AuthorizationsTable.kt
@@ -1,13 +1,12 @@
-package io.timemates.backend.data.authorization.db.table
+package org.timemates.backend.auth.data.db.table
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.data.authorization.db.entities.DbAuthorization.Permissions.GrantLevel
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource
import org.jetbrains.exposed.sql.Table
+import org.timemates.backend.auth.data.db.entities.DbAuthorization.Permissions.GrantLevel
+import org.timemates.backend.types.auth.value.AccessHash
object AuthorizationsTable : Table("authorizations") {
val AUTHORIZATION_ID = integer("authorization_id").autoIncrement()
- val USER_ID = long("user_id").references(PostgresqlUsersDataSource.UsersTable.USER_ID)
+ val USER_ID = long("user_id")
val ACCESS_TOKEN = varchar("access_token", AccessHash.SIZE)
val REFRESH_TOKEN = varchar("refresh_token", AccessHash.SIZE)
val EXPIRES_AT = long("access_token_expires_at")
@@ -22,8 +21,6 @@ object AuthorizationsTable : Table("authorizations") {
.default(GrantLevel.NOT_GRANTED)
val USERS_PERMISSION = enumeration("users_permission")
.default(GrantLevel.NOT_GRANTED)
- val FILES_PERMISSION = enumeration("files_permission")
- .default(GrantLevel.NOT_GRANTED)
override val primaryKey: PrimaryKey = PrimaryKey(USER_ID, AUTHORIZATION_ID)
}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/table/VerificationSessionsTable.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/table/VerificationSessionsTable.kt
similarity index 60%
rename from data/src/main/kotlin/io/timemates/backend/data/authorization/db/table/VerificationSessionsTable.kt
rename to features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/table/VerificationSessionsTable.kt
index 917f8a8b..bb5cfa0b 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/authorization/db/table/VerificationSessionsTable.kt
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/db/table/VerificationSessionsTable.kt
@@ -1,13 +1,12 @@
-package io.timemates.backend.data.authorization.db.table
+package org.timemates.backend.auth.data.db.table
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.authorization.types.value.VerificationHash
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource.UsersTable
-import io.timemates.backend.users.types.value.EmailAddress
import org.jetbrains.exposed.sql.Table
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.auth.value.VerificationHash
+import org.timemates.backend.types.users.value.EmailAddress
object VerificationSessionsTable : Table("verification_sessions") {
- val USER_ID = long("user_id").references(UsersTable.USER_ID).nullable()
+ val USER_ID = long("user_id").nullable()
val VERIFICATION_HASH = varchar("verification_hash", VerificationHash.SIZE)
val EMAIL = varchar("email", EmailAddress.SIZE.last)
val IS_CONFIRMED = bool("is_confirmed").default(false)
diff --git a/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/mapper/AuthorizationsMapper.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/mapper/AuthorizationsMapper.kt
new file mode 100644
index 00000000..251c395b
--- /dev/null
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/mapper/AuthorizationsMapper.kt
@@ -0,0 +1,152 @@
+package org.timemates.backend.auth.data.mapper
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.auth.data.cache.entities.CacheAuthorization
+import org.timemates.backend.auth.data.db.entities.DbAuthorization
+import org.timemates.backend.foundation.authorization.Scope
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.AuthorizationsScope
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.metadata.value.ClientIpAddress
+import org.timemates.backend.types.auth.metadata.value.ClientName
+import org.timemates.backend.types.auth.metadata.value.ClientVersion
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.users.UsersScope
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+import org.timemates.backend.auth.data.cache.entities.CacheAuthorization.Permissions.GrantLevel as CacheGrantLevel
+import org.timemates.backend.auth.data.db.entities.DbAuthorization.Permissions.GrantLevel as DbGrantLevel
+
+@OptIn(ValidationDelicateApi::class)
+class AuthorizationsMapper {
+ fun dbAuthToDomainAuth(auth: DbAuthorization): Authorization = with(auth) {
+ return Authorization(
+ userId = UserId.createUnsafe(auth.userId),
+ accessHash = AccessHash.createUnsafe(auth.accessHash),
+ refreshAccessHash = RefreshHash.createUnsafe(auth.refreshAccessHash),
+ scopes = dbPermissionsToDomain(permissions),
+ expiresAt = UnixTime.createUnsafe(expiresAt),
+ createdAt = UnixTime.createUnsafe(createdAt),
+ clientMetadata = ClientMetadata(
+ clientName = ClientName.createUnsafe(auth.metaClientName),
+ clientVersion = ClientVersion.createUnsafe(auth.metaClientVersion),
+ clientIpAddress = ClientIpAddress.createUnsafe(auth.metaClientIpAddress),
+ )
+ )
+ }
+
+ fun dbAuthToCacheAuth(auth: DbAuthorization): CacheAuthorization = with(auth) {
+ return CacheAuthorization(
+ userId = userId,
+ accessHash = accessHash,
+ refreshAccessHash = refreshAccessHash,
+ permissions = dbPermissionsToCachePermissions(permissions),
+ expiresAt = expiresAt,
+ createdAt = createdAt,
+ clientMetadata = ClientMetadata(
+ clientName = ClientName.createUnsafe(auth.metaClientName),
+ clientVersion = ClientVersion.createUnsafe(auth.metaClientVersion),
+ clientIpAddress = ClientIpAddress.createUnsafe(auth.metaClientIpAddress),
+ ),
+ )
+ }
+
+ private fun dbPermissionsToCachePermissions(
+ auth: DbAuthorization.Permissions,
+ ): CacheAuthorization.Permissions = with(auth) {
+ return CacheAuthorization.Permissions(
+ dbGrantLevelToCacheGrantLevel(authorization),
+ dbGrantLevelToCacheGrantLevel(users),
+ dbGrantLevelToCacheGrantLevel(timers),
+ )
+ }
+
+ private fun dbGrantLevelToCacheGrantLevel(
+ level: DbGrantLevel,
+ ): CacheAuthorization.Permissions.GrantLevel {
+ return when (level) {
+ DbGrantLevel.READ -> CacheAuthorization.Permissions.GrantLevel.READ
+ DbGrantLevel.WRITE -> CacheAuthorization.Permissions.GrantLevel.WRITE
+ DbGrantLevel.NOT_GRANTED -> CacheAuthorization.Permissions.GrantLevel.NOT_GRANTED
+ }
+ }
+
+ fun cacheAuthToDomainAuth(auth: CacheAuthorization): Authorization = with(auth) {
+ return Authorization(
+ userId = UserId.createUnsafe(auth.userId),
+ accessHash = AccessHash.createUnsafe(auth.accessHash),
+ refreshAccessHash = RefreshHash.createUnsafe(auth.refreshAccessHash),
+ scopes = cachePermissionsToDomain(permissions),
+ expiresAt = UnixTime.createUnsafe(expiresAt),
+ createdAt = UnixTime.createUnsafe(createdAt),
+ clientMetadata = ClientMetadata(
+ clientName = ClientName.createUnsafe(auth.clientMetadata.clientName.string),
+ clientVersion = ClientVersion.createUnsafe(auth.clientMetadata.clientVersion.double),
+ clientIpAddress = ClientIpAddress.createUnsafe(auth.clientMetadata.clientIpAddress.string),
+ )
+ )
+ }
+
+ private fun dbPermissionsToDomain(
+ dbPermissions: DbAuthorization.Permissions,
+ ): List = with(dbPermissions) {
+ return buildList {
+ if (authorization != DbGrantLevel.NOT_GRANTED) {
+ add(
+ if (authorization == DbGrantLevel.WRITE)
+ AuthorizationsScope.Write
+ else AuthorizationsScope.Read
+ )
+ }
+
+ if (users != DbGrantLevel.NOT_GRANTED) {
+ add(
+ if (users == DbGrantLevel.WRITE)
+ UsersScope.Write
+ else UsersScope.Read
+ )
+ }
+
+ if (timers != DbGrantLevel.NOT_GRANTED) {
+ add(
+ if (timers == DbGrantLevel.WRITE)
+ TimersScope.Write
+ else TimersScope.Read
+ )
+ }
+ }
+ }
+
+ private fun cachePermissionsToDomain(
+ cache: CacheAuthorization.Permissions,
+ ): List = with(cache) {
+ return buildList {
+ if (authorization != CacheGrantLevel.NOT_GRANTED) {
+ add(
+ if (authorization == CacheGrantLevel.WRITE)
+ AuthorizationsScope.Write
+ else AuthorizationsScope.Read
+ )
+ }
+
+ if (users != CacheGrantLevel.NOT_GRANTED) {
+ add(
+ if (users == CacheGrantLevel.WRITE)
+ UsersScope.Write
+ else UsersScope.Read
+ )
+ }
+
+ if (timers != CacheGrantLevel.NOT_GRANTED) {
+ add(
+ if (timers == CacheGrantLevel.WRITE)
+ TimersScope.Write
+ else TimersScope.Read
+ )
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/mapper/VerificationsMapper.kt b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/mapper/VerificationsMapper.kt
new file mode 100644
index 00000000..0deda24a
--- /dev/null
+++ b/features/auth/data/src/main/kotlin/org/timemates/backend/auth/data/mapper/VerificationsMapper.kt
@@ -0,0 +1,32 @@
+package org.timemates.backend.auth.data.mapper
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.auth.data.db.entities.DbVerification
+import org.timemates.backend.types.auth.Verification
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.metadata.value.ClientIpAddress
+import org.timemates.backend.types.auth.metadata.value.ClientName
+import org.timemates.backend.types.auth.metadata.value.ClientVersion
+import org.timemates.backend.types.auth.value.Attempts
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.users.value.EmailAddress
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+
+@OptIn(ValidationDelicateApi::class)
+class VerificationsMapper {
+ fun dbToDomain(dbVerification: DbVerification): Verification {
+ return Verification(
+ emailAddress = EmailAddress.createUnsafe(dbVerification.emailAddress),
+ code = VerificationCode.createUnsafe(dbVerification.code),
+ attempts = Attempts.createUnsafe(dbVerification.attempts),
+ time = UnixTime.createUnsafe(dbVerification.time),
+ isConfirmed = dbVerification.isConfirmed,
+ clientMetadata = ClientMetadata(
+ clientName = ClientName.createUnsafe(dbVerification.metaClientName),
+ clientVersion = ClientVersion.createUnsafe(dbVerification.metaClientVersion),
+ clientIpAddress = ClientIpAddress.createUnsafe(dbVerification.metaClientIpAddress),
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/data/src/test/kotlin/org/timemates/backend/auth/data/test/db/TableAuthorizationsDataSourceTest.kt b/features/auth/data/src/test/kotlin/org/timemates/backend/auth/data/test/db/TableAuthorizationsDataSourceTest.kt
new file mode 100644
index 00000000..a735c072
--- /dev/null
+++ b/features/auth/data/src/test/kotlin/org/timemates/backend/auth/data/test/db/TableAuthorizationsDataSourceTest.kt
@@ -0,0 +1,177 @@
+package org.timemates.backend.auth.data.test.db
+
+import kotlinx.coroutines.test.runTest
+import org.jetbrains.exposed.sql.Database
+import org.timemates.backend.auth.data.db.TableAuthorizationsDataSource
+import org.timemates.backend.auth.data.db.entities.DbAuthorization
+import org.timemates.backend.auth.data.db.mapper.DbAuthorizationsMapper
+import kotlin.test.*
+import kotlin.time.Duration.Companion.minutes
+
+class TableAuthorizationsDataSourceTest {
+ private val databaseUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;"
+ private val databaseDriver = "org.h2.Driver"
+
+ private val database = Database.connect(databaseUrl, databaseDriver)
+ private val datasource = TableAuthorizationsDataSource(database, DbAuthorizationsMapper())
+
+ @BeforeTest
+ @AfterTest
+ fun `clean table`() = runTest {
+ datasource.clearAll()
+ }
+
+ @Test
+ fun `check createAuthorization saves correctly`() = runTest {
+ val expected = DbAuthorization(
+ 1,
+ 0,
+ "test-access-hash", // for tests length doesn't matter
+ "test-refresh-hash", // for tests length doesn't matter
+ permissions = DbAuthorization.Permissions.All,
+ expiresAt = System.currentTimeMillis() + 1.minutes.inWholeMilliseconds,
+ createdAt = System.currentTimeMillis(),
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost",
+ )
+
+ val id = datasource.createAuthorization(
+ userId = expected.userId,
+ accessHash = expected.accessHash,
+ refreshAccessHash = expected.refreshAccessHash,
+ permissions = expected.permissions,
+ expiresAt = expected.expiresAt,
+ createdAt = expected.createdAt,
+ metaClientName = expected.metaClientName,
+ metaClientVersion = expected.metaClientVersion,
+ metaClientIpAddress = expected.metaClientIpAddress,
+ )
+
+ assertEquals(
+ expected = expected.copy(authorizationId = id),
+ actual = datasource.getAuthorizations(0, null)
+ .value.firstOrNull(),
+ )
+ }
+
+ @Test
+ fun `check renewToken works correctly`() = runTest {
+ val initial = DbAuthorization(
+ 1,
+ 0,
+ "test-access-hash", // for tests length doesn't matter
+ "test-refresh-hash", // for tests length doesn't matter
+ permissions = DbAuthorization.Permissions.All,
+ expiresAt = System.currentTimeMillis() + 1.minutes.inWholeMilliseconds,
+ createdAt = System.currentTimeMillis(),
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost",
+ )
+
+ val id = datasource.createAuthorization(
+ userId = initial.userId,
+ accessHash = initial.accessHash,
+ refreshAccessHash = initial.refreshAccessHash,
+ permissions = initial.permissions,
+ expiresAt = initial.expiresAt,
+ createdAt = initial.createdAt,
+ metaClientName = initial.metaClientName,
+ metaClientVersion = initial.metaClientVersion,
+ metaClientIpAddress = initial.metaClientIpAddress,
+ )
+
+ val newAccessHash = "new-access-hash"
+ datasource.renewAccessHash(initial.refreshAccessHash, newAccessHash, Long.MAX_VALUE)
+
+ assertEquals(
+ expected = initial.copy(authorizationId = id, accessHash = newAccessHash, expiresAt = Long.MAX_VALUE),
+ actual = datasource.getAuthorizations(0, null)
+ .value.firstOrNull(),
+ )
+ }
+
+ @Test
+ fun `check removeAuthorization works correctly`() = runTest {
+ val auth = DbAuthorization(
+ 1,
+ 0,
+ "test-access-hash", // for tests length doesn't matter
+ "test-refresh-hash", // for tests length doesn't matter
+ permissions = DbAuthorization.Permissions.All,
+ expiresAt = System.currentTimeMillis() + 1.minutes.inWholeMilliseconds,
+ createdAt = System.currentTimeMillis(),
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost",
+ )
+
+ datasource.createAuthorization(
+ userId = auth.userId,
+ accessHash = auth.accessHash,
+ refreshAccessHash = auth.refreshAccessHash,
+ permissions = auth.permissions,
+ expiresAt = auth.expiresAt,
+ createdAt = auth.createdAt,
+ metaClientName = auth.metaClientName,
+ metaClientVersion = auth.metaClientVersion,
+ metaClientIpAddress = auth.metaClientIpAddress,
+ )
+
+ datasource.removeAuthorization(auth.accessHash)
+
+ assert(datasource.getAuthorizations(0, null).value.isEmpty())
+ }
+
+ @Test
+ fun `check getAuthorizations works correctly`() = runTest {
+ val userId = 0L
+
+ // Create some authorization records
+ val auth1 = createAuth(userId, "access-hash-1")
+ val auth2 = createAuth(userId, "access-hash-2")
+ val auth3 = createAuth(userId, "access-hash-3")
+
+ // Get the first page
+ val firstPage = datasource.getAuthorizations(userId, null)
+
+ assertContentEquals(
+ expected = listOf(auth1, auth2, auth3),
+ actual = firstPage.value
+ )
+
+ // Get the second page
+ val nextPageToken1 = firstPage.nextPageToken
+ assertNotNull(nextPageToken1)
+ }
+
+ private suspend fun createAuth(userId: Long, accessHash: String): DbAuthorization {
+ val auth = DbAuthorization(
+ authorizationId = 0,
+ userId = userId,
+ accessHash = accessHash,
+ refreshAccessHash = "test-refresh-hash",
+ permissions = DbAuthorization.Permissions.All,
+ expiresAt = System.currentTimeMillis() + 1.minutes.inWholeMilliseconds,
+ createdAt = System.currentTimeMillis(),
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost",
+ )
+
+ val id = datasource.createAuthorization(
+ userId = auth.userId,
+ accessHash = auth.accessHash,
+ refreshAccessHash = auth.refreshAccessHash,
+ permissions = auth.permissions,
+ expiresAt = auth.expiresAt,
+ createdAt = auth.createdAt,
+ metaClientName = auth.metaClientName,
+ metaClientVersion = auth.metaClientVersion,
+ metaClientIpAddress = auth.metaClientIpAddress,
+ )
+
+ return auth.copy(authorizationId = id)
+ }
+}
\ No newline at end of file
diff --git a/features/auth/data/src/test/kotlin/org/timemates/backend/auth/data/test/db/TableVerificationsDataSourceTest.kt b/features/auth/data/src/test/kotlin/org/timemates/backend/auth/data/test/db/TableVerificationsDataSourceTest.kt
new file mode 100644
index 00000000..814f6b8a
--- /dev/null
+++ b/features/auth/data/src/test/kotlin/org/timemates/backend/auth/data/test/db/TableVerificationsDataSourceTest.kt
@@ -0,0 +1,233 @@
+package org.timemates.backend.auth.data.test.db
+
+import kotlinx.coroutines.test.runTest
+import org.jetbrains.exposed.sql.Database
+import org.timemates.backend.auth.data.db.TableVerificationsDataSource
+import org.timemates.backend.auth.data.db.entities.DbVerification
+import org.timemates.backend.auth.data.db.mapper.DbVerificationsMapper
+import kotlin.test.*
+
+class TableVerificationsDataSourceTest {
+ private val databaseUrl = "jdbc:h2:mem:regular;DB_CLOSE_DELAY=-1;"
+ private val databaseDriver = "org.h2.Driver"
+
+ private val database = Database.connect(databaseUrl, databaseDriver)
+ private val verificationsMapper = DbVerificationsMapper()
+
+ private val datasource = TableVerificationsDataSource(database, verificationsMapper)
+
+ @BeforeTest
+ @AfterTest
+ fun `clean table`() = runTest {
+ datasource.clearAll()
+ }
+
+ @Test
+ fun `check add creates a verification session correctly`() = runTest {
+ val emailAddress = "test@example.com"
+ val verificationToken = "verification-token"
+ val code = "123456"
+ val time = System.currentTimeMillis()
+ val attempts = 3
+ val metaClientName = "test-client"
+ val metaClientVersion = 1.0
+ val metaClientIpAddress = "localhost"
+
+ datasource.add(
+ emailAddress = emailAddress,
+ verificationToken = verificationToken,
+ code = code,
+ time = time,
+ attempts = attempts,
+ metaClientName = metaClientName,
+ metaClientVersion = metaClientVersion,
+ metaClientIpAddress = metaClientIpAddress
+ )
+
+ val result = datasource.getVerification(verificationToken)
+
+ assertNotNull(result)
+ assertEquals(expected = emailAddress, actual = result.emailAddress)
+ assertEquals(expected = verificationToken, actual = result.verificationHash)
+ assertFalse(result.isConfirmed)
+ assertEquals(expected = code, actual = result.code)
+ assertEquals(expected = attempts, actual = result.attempts)
+ assertEquals(expected = time, actual = result.time)
+ assertEquals(expected = metaClientName, actual = result.metaClientName)
+ assertEquals(expected = metaClientVersion, actual = result.metaClientVersion)
+ assertEquals(expected = metaClientIpAddress, actual = result.metaClientIpAddress)
+ }
+
+ @Test
+ fun `check getVerification returns correct result`() = runTest {
+ val verificationHash = "verification-hash"
+ val dbVerification = DbVerification(
+ emailAddress = "test@example.com",
+ verificationHash = verificationHash,
+ isConfirmed = false,
+ code = "123456",
+ attempts = 3,
+ time = System.currentTimeMillis(),
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost",
+ )
+
+ datasource.add(
+ emailAddress = dbVerification.emailAddress,
+ verificationToken = dbVerification.verificationHash,
+ code = dbVerification.code,
+ time = dbVerification.time,
+ attempts = dbVerification.attempts,
+ metaClientName = dbVerification.metaClientName,
+ metaClientVersion = dbVerification.metaClientVersion,
+ metaClientIpAddress = dbVerification.metaClientIpAddress,
+ )
+
+ val result = datasource.getVerification(verificationHash)
+
+ assertEquals(expected = dbVerification, actual = result)
+ }
+
+ @Test
+ fun `check decreaseAttempts updates attempts correctly`() = runTest {
+ val verificationHash = "verification-hash"
+ val initialAttempts = 3
+
+ datasource.add(
+ emailAddress = "test@example.com",
+ verificationToken = verificationHash,
+ code = "123456",
+ time = System.currentTimeMillis(),
+ attempts = initialAttempts,
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost"
+ )
+
+ datasource.decreaseAttempts(verificationHash)
+
+ val result = datasource.getVerification(verificationHash)
+
+ assertNotNull(result)
+ assertEquals(expected = initialAttempts - 1, actual = result.attempts)
+ }
+
+ @Test
+ fun `check getAttempts returns correct sum of attempts`() = runTest {
+ val emailAddress = "test@example.com"
+ val afterTime = System.currentTimeMillis()
+
+ datasource.add(
+ emailAddress = emailAddress,
+ verificationToken = "verification-hash1",
+ code = "123456",
+ time = afterTime + 1000,
+ attempts = 5,
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost"
+ )
+ datasource.add(
+ emailAddress = emailAddress,
+ verificationToken = "verification-hash2",
+ code = "123456",
+ time = afterTime + 2000,
+ attempts = 2,
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost"
+ )
+
+ val result = datasource.getAttempts(emailAddress, afterTime)
+
+ assertEquals(expected = 7, actual = result)
+ }
+
+ @Test
+ fun `check getSessionsCount returns correct count`() = runTest {
+ val emailAddress = "test@example.com"
+ val afterTime = System.currentTimeMillis()
+
+ datasource.add(
+ emailAddress = emailAddress,
+ verificationToken = "verification-hash1",
+ code = "123456",
+ time = afterTime + 1000,
+ attempts = 1,
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost"
+ )
+ datasource.add(
+ emailAddress = emailAddress,
+ verificationToken = "verification-hash2",
+ code = "123456",
+ time = afterTime + 2000,
+ attempts = 1,
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost"
+ )
+
+ val result = datasource.getSessionsCount(emailAddress, afterTime)
+
+ assertEquals(expected = 2, actual = result)
+ }
+
+ @Test
+ fun `check setAsConfirmed updates verification session correctly`() = runTest {
+ val verificationHash = "verification-hash"
+
+ datasource.add(
+ emailAddress = "test@example.com",
+ verificationToken = verificationHash,
+ code = "123456",
+ time = System.currentTimeMillis(),
+ attempts = 1,
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost"
+ )
+
+ val result = datasource.setAsConfirmed(verificationHash)
+
+ assertTrue(result)
+
+ val updatedSession = datasource.getVerification(verificationHash)
+
+ assertNotNull(updatedSession)
+ assertTrue(updatedSession.isConfirmed)
+ }
+
+ @Test
+ fun `check setAsConfirmed returns false if verification session does not exist`() = runTest {
+ val verificationHash = "non-existent-hash"
+
+ val result = datasource.setAsConfirmed(verificationHash)
+
+ assertFalse(result)
+ }
+
+ @Test
+ fun `check remove deletes verification session correctly`() = runTest {
+ val verificationHash = "verification-hash"
+
+ datasource.add(
+ emailAddress = "test@example.com",
+ verificationToken = verificationHash,
+ code = "123456",
+ time = System.currentTimeMillis(),
+ attempts = 1,
+ metaClientName = "test-client",
+ metaClientVersion = 1.0,
+ metaClientIpAddress = "localhost"
+ )
+
+ datasource.remove(verificationHash)
+
+ val result = datasource.getVerification(verificationHash)
+
+ assertNull(result)
+ }
+}
diff --git a/features/auth/dependencies/build.gradle.kts b/features/auth/dependencies/build.gradle.kts
new file mode 100644
index 00000000..4bfb0e4d
--- /dev/null
+++ b/features/auth/dependencies/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+ alias(libs.plugins.ksp)
+}
+
+dependencies {
+ implementation(projects.features.auth.domain)
+ implementation(projects.features.auth.data)
+ implementation(projects.features.auth.adapters)
+
+ implementation(projects.features.users.domain)
+
+ implementation(projects.foundation.random)
+
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.ktor.client.core)
+
+ implementation(libs.exposed.core)
+ implementation(libs.koin.core)
+ implementation(libs.koin.annotations)
+ ksp(libs.koin.ksp.compiler)
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/AuthorizationsModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/AuthorizationsModule.kt
new file mode 100644
index 00000000..e357d40c
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/AuthorizationsModule.kt
@@ -0,0 +1,31 @@
+package org.timemates.backend.auth.deps
+
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.deps.repositories.AuthorizationsRepositoriesModule
+import org.timemates.backend.auth.deps.repositories.cache.CacheAuthorizationsModule
+import org.timemates.backend.auth.deps.repositories.database.DatabaseAuthorizationModule
+import org.timemates.backend.auth.deps.repositories.mailer.MailerModule
+import org.timemates.backend.auth.deps.repositories.mappers.AuthorizationsMappersModule
+import org.timemates.backend.auth.deps.usecases.*
+
+@Module(
+ includes = [
+ // Data-related modules
+ CacheAuthorizationsModule::class,
+ DatabaseAuthorizationModule::class,
+ MailerModule::class,
+ AuthorizationsMappersModule::class,
+ AuthorizationsRepositoriesModule::class,
+
+ // UseCases
+ AuthByEmailUseCaseModule::class,
+ ConfigureNewAccountUseCaseModule::class,
+ GetAuthorizationsUseCaseModule::class,
+ GetAuthorizationUseCaseModule::class,
+ GetUserIdByAccessTokenUseCaseModule::class,
+ RefreshAccessTokenUseCaseModule::class,
+ RemoveAccessTokenUseCaseModule::class,
+ VerifyAuthorizationUseCaseModule::class,
+ ],
+)
+class AuthorizationsModule
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/AuthorizationsRepositoriesModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/AuthorizationsRepositoriesModule.kt
new file mode 100644
index 00000000..23a7c811
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/AuthorizationsRepositoriesModule.kt
@@ -0,0 +1,49 @@
+package org.timemates.backend.auth.deps.repositories
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.data.PostgresqlAuthorizationsRepository
+import org.timemates.backend.auth.data.PostgresqlVerificationsRepository
+import org.timemates.backend.auth.data.cache.CacheAuthorizationsDataSource
+import org.timemates.backend.auth.data.db.TableAuthorizationsDataSource
+import org.timemates.backend.auth.data.db.TableVerificationsDataSource
+import org.timemates.backend.auth.data.mapper.AuthorizationsMapper
+import org.timemates.backend.auth.data.mapper.VerificationsMapper
+import org.timemates.backend.auth.deps.repositories.adapter.AdapterRepositoriesModule
+import org.timemates.backend.auth.deps.repositories.cache.CacheAuthorizationsModule
+import org.timemates.backend.auth.deps.repositories.database.DatabaseAuthorizationModule
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+
+@Module(
+ includes = [
+ CacheAuthorizationsModule::class,
+ DatabaseAuthorizationModule::class,
+ AdapterRepositoriesModule::class,
+ ]
+)
+class AuthorizationsRepositoriesModule {
+ @Factory
+ fun authorizationsRepository(
+ tableAuthorizationsDataSource: TableAuthorizationsDataSource,
+ cacheAuthorizationsDataSource: CacheAuthorizationsDataSource,
+ authorizationsMapper: AuthorizationsMapper,
+ ): AuthorizationsRepository {
+ return PostgresqlAuthorizationsRepository(
+ tableAuthorizationsDataSource,
+ cacheAuthorizationsDataSource,
+ authorizationsMapper,
+ )
+ }
+
+ @Factory
+ fun verificationsRepository(
+ tableVerificationsDataSource: TableVerificationsDataSource,
+ verificationsMapper: VerificationsMapper,
+ ): VerificationsRepository {
+ return PostgresqlVerificationsRepository(
+ tableVerificationsDataSource,
+ verificationsMapper,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/adapter/AdapterRepositoriesModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/adapter/AdapterRepositoriesModule.kt
new file mode 100644
index 00000000..55f6944b
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/adapter/AdapterRepositoriesModule.kt
@@ -0,0 +1,13 @@
+package org.timemates.backend.auth.deps.repositories.adapter
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.adapters.UsersRepositoryDelegateAdapter
+import org.timemates.backend.auth.domain.repositories.UsersRepository
+import org.timemates.backend.users.domain.repositories.UsersRepository as UsersRepositoryAdapter
+
+@Module
+class AdapterRepositoriesModule {
+ @Factory
+ fun usersRepository(delegate: UsersRepositoryAdapter): UsersRepository = UsersRepositoryDelegateAdapter(delegate)
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/cache/CacheAuthorizationsModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/cache/CacheAuthorizationsModule.kt
new file mode 100644
index 00000000..0fe31b7c
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/cache/CacheAuthorizationsModule.kt
@@ -0,0 +1,20 @@
+package org.timemates.backend.auth.deps.repositories.cache
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.timemates.backend.auth.data.cache.CacheAuthorizationsDataSource
+import kotlin.time.Duration
+
+@Module
+class CacheAuthorizationsModule {
+ @Factory
+ fun cacheAuthorizationsDs(
+ @Named("auth.cache.size")
+ maxCacheEntities: Long,
+ @Named("auth.cache.alive")
+ maxAliveTime: Duration,
+ ): CacheAuthorizationsDataSource {
+ return CacheAuthorizationsDataSource(maxCacheEntities, maxAliveTime)
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/database/DatabaseAuthorizationModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/database/DatabaseAuthorizationModule.kt
new file mode 100644
index 00000000..25727d67
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/database/DatabaseAuthorizationModule.kt
@@ -0,0 +1,29 @@
+package org.timemates.backend.auth.deps.repositories.database
+
+import org.jetbrains.exposed.sql.Database
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.data.db.TableAuthorizationsDataSource
+import org.timemates.backend.auth.data.db.TableVerificationsDataSource
+import org.timemates.backend.auth.data.db.mapper.DbAuthorizationsMapper
+import org.timemates.backend.auth.data.db.mapper.DbVerificationsMapper
+import org.timemates.backend.auth.deps.repositories.mappers.AuthorizationsMappersModule
+
+@Module(includes = [AuthorizationsMappersModule::class])
+class DatabaseAuthorizationModule {
+ @Factory
+ fun authorizationTableDs(
+ database: Database,
+ mapper: DbAuthorizationsMapper,
+ ): TableAuthorizationsDataSource {
+ return TableAuthorizationsDataSource(database, mapper)
+ }
+
+ @Factory
+ fun verificationsTableDs(
+ database: Database,
+ mapper: DbVerificationsMapper,
+ ): TableVerificationsDataSource {
+ return TableVerificationsDataSource(database, mapper)
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/mailer/MailerModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/mailer/MailerModule.kt
new file mode 100644
index 00000000..486cb65c
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/mailer/MailerModule.kt
@@ -0,0 +1,38 @@
+package org.timemates.backend.auth.deps.repositories.mailer
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.timemates.backend.auth.adapters.MailerSendEmailRepositoryAdapter
+import org.timemates.backend.auth.domain.repositories.EmailRepository
+
+@Module
+class MailerModule {
+ @Factory
+ fun configuration(
+ @Named("mailersend.apiKey")
+ apiKey: String,
+ @Named("mailersend.sender")
+ sender: String,
+ @Named("mailersend.templates.confirmation")
+ confirmationTemplateId: String,
+ @Named("mailersend.supportEmail")
+ supportEmail: String,
+ ): MailerSendEmailRepositoryAdapter.Configuration {
+ return MailerSendEmailRepositoryAdapter.Configuration(
+ apiKey, sender, confirmationTemplateId, supportEmail,
+ )
+ }
+
+ @Factory
+ fun emailRepository(
+ configuration: MailerSendEmailRepositoryAdapter.Configuration,
+ @Named("application.isDebug")
+ isDebug: Boolean,
+ ): EmailRepository {
+ return MailerSendEmailRepositoryAdapter(
+ configuration = configuration,
+ isDebug = isDebug,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/mappers/AuthorizationsMappersModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/mappers/AuthorizationsMappersModule.kt
new file mode 100644
index 00000000..b48149c1
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/repositories/mappers/AuthorizationsMappersModule.kt
@@ -0,0 +1,31 @@
+package org.timemates.backend.auth.deps.repositories.mappers
+
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Singleton
+import org.timemates.backend.auth.data.db.mapper.DbAuthorizationsMapper
+import org.timemates.backend.auth.data.db.mapper.DbVerificationsMapper
+import org.timemates.backend.auth.data.mapper.AuthorizationsMapper
+import org.timemates.backend.auth.data.mapper.VerificationsMapper
+
+@Module
+class AuthorizationsMappersModule {
+ @Singleton
+ fun dbAuthMapper(): DbAuthorizationsMapper {
+ return DbAuthorizationsMapper()
+ }
+
+ @Singleton
+ fun dbVerificationsMapper(): DbVerificationsMapper {
+ return DbVerificationsMapper()
+ }
+
+ @Singleton
+ fun verificationsMapper(): VerificationsMapper {
+ return VerificationsMapper()
+ }
+
+ @Singleton
+ fun authMapper(): AuthorizationsMapper {
+ return AuthorizationsMapper()
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/AuthByEmailUseCaseModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/AuthByEmailUseCaseModule.kt
new file mode 100644
index 00000000..a99a4d2a
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/AuthByEmailUseCaseModule.kt
@@ -0,0 +1,27 @@
+package org.timemates.backend.auth.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import com.timemates.random.RandomProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.domain.repositories.EmailRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.auth.domain.usecases.AuthByEmailUseCase
+
+@Module
+class AuthByEmailUseCaseModule {
+ @Factory
+ fun authByEmailUseCase(
+ emailRepository: EmailRepository,
+ verificationsRepository: VerificationsRepository,
+ timeProvider: TimeProvider,
+ randomProvider: RandomProvider,
+ ): AuthByEmailUseCase {
+ return AuthByEmailUseCase(
+ emailRepository,
+ verificationsRepository,
+ timeProvider,
+ randomProvider,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/ConfigureNewAccountUseCaseModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/ConfigureNewAccountUseCaseModule.kt
new file mode 100644
index 00000000..072529f5
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/ConfigureNewAccountUseCaseModule.kt
@@ -0,0 +1,30 @@
+package org.timemates.backend.auth.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import com.timemates.random.RandomProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.repositories.UsersRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.auth.domain.usecases.ConfigureNewAccountUseCase
+
+@Module
+class ConfigureNewAccountUseCaseModule {
+ @Factory
+ fun configureNewAccountUseCase(
+ usersRepository: UsersRepository,
+ authorizationsRepository: AuthorizationsRepository,
+ verificationsRepository: VerificationsRepository,
+ timeProvider: TimeProvider,
+ randomProvider: RandomProvider,
+ ): ConfigureNewAccountUseCase {
+ return ConfigureNewAccountUseCase(
+ usersRepository,
+ authorizationsRepository,
+ verificationsRepository,
+ timeProvider,
+ randomProvider,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetAuthorizationUseCaseModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetAuthorizationUseCaseModule.kt
new file mode 100644
index 00000000..d2ce42db
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetAuthorizationUseCaseModule.kt
@@ -0,0 +1,21 @@
+package org.timemates.backend.auth.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.GetAuthorizationUseCase
+
+@Module
+class GetAuthorizationUseCaseModule {
+ @Factory
+ fun getAuthorizationUseCase(
+ authorizationsRepository: AuthorizationsRepository,
+ timeProvider: TimeProvider,
+ ): GetAuthorizationUseCase {
+ return GetAuthorizationUseCase(
+ authorizationsRepository,
+ timeProvider,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetAuthorizationsUseCaseModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetAuthorizationsUseCaseModule.kt
new file mode 100644
index 00000000..6b44934f
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetAuthorizationsUseCaseModule.kt
@@ -0,0 +1,18 @@
+package org.timemates.backend.auth.deps.usecases
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.GetAuthorizationsUseCase
+
+@Module
+class GetAuthorizationsUseCaseModule {
+ @Factory
+ fun getAuthorizationsUseCase(
+ authorizationsRepository: AuthorizationsRepository,
+ ): GetAuthorizationsUseCase {
+ return GetAuthorizationsUseCase(
+ authorizationsRepository,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetUserIdByAccessTokenUseCaseModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetUserIdByAccessTokenUseCaseModule.kt
new file mode 100644
index 00000000..dbb6fccb
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/GetUserIdByAccessTokenUseCaseModule.kt
@@ -0,0 +1,21 @@
+package org.timemates.backend.auth.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.GetUserIdByAccessTokenUseCase
+
+@Module
+class GetUserIdByAccessTokenUseCaseModule {
+ @Factory
+ fun getUserIdByAccessTokenUseCase(
+ authorizationsRepository: AuthorizationsRepository,
+ timeProvider: TimeProvider,
+ ): GetUserIdByAccessTokenUseCase {
+ return GetUserIdByAccessTokenUseCase(
+ authorizationsRepository,
+ timeProvider,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/RefreshAccessTokenUseCaseModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/RefreshAccessTokenUseCaseModule.kt
new file mode 100644
index 00000000..09a27192
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/RefreshAccessTokenUseCaseModule.kt
@@ -0,0 +1,24 @@
+package org.timemates.backend.auth.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import com.timemates.random.RandomProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.RefreshAccessTokenUseCase
+
+@Module
+class RefreshAccessTokenUseCaseModule {
+ @Factory
+ fun removeAccessTokenUseCase(
+ authorizationsRepository: AuthorizationsRepository,
+ randomProvider: RandomProvider,
+ timeProvider: TimeProvider,
+ ): RefreshAccessTokenUseCase {
+ return RefreshAccessTokenUseCase(
+ randomProvider,
+ authorizationsRepository,
+ timeProvider,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/RemoveAccessTokenUseCaseModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/RemoveAccessTokenUseCaseModule.kt
new file mode 100644
index 00000000..94ed4b96
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/RemoveAccessTokenUseCaseModule.kt
@@ -0,0 +1,18 @@
+package org.timemates.backend.auth.deps.usecases
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.RemoveAccessTokenUseCase
+
+@Module
+class RemoveAccessTokenUseCaseModule {
+ @Factory
+ fun removeAccessTokenUseCase(
+ authorizationsRepository: AuthorizationsRepository,
+ ): RemoveAccessTokenUseCase {
+ return RemoveAccessTokenUseCase(
+ authorizationsRepository,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/VerifyAuthorizationUseCaseModule.kt b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/VerifyAuthorizationUseCaseModule.kt
new file mode 100644
index 00000000..99a58c6c
--- /dev/null
+++ b/features/auth/dependencies/src/main/kotlin/org/timemates/backend/auth/deps/usecases/VerifyAuthorizationUseCaseModule.kt
@@ -0,0 +1,30 @@
+package org.timemates.backend.auth.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import com.timemates.random.RandomProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.repositories.UsersRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.auth.domain.usecases.VerifyAuthorizationUseCase
+
+@Module
+class VerifyAuthorizationUseCaseModule {
+ @Factory
+ fun verifyAuthorizationUseCase(
+ usersRepository: UsersRepository,
+ authorizationsRepository: AuthorizationsRepository,
+ verificationsRepository: VerificationsRepository,
+ timeProvider: TimeProvider,
+ randomProvider: RandomProvider,
+ ): VerifyAuthorizationUseCase {
+ return VerifyAuthorizationUseCase(
+ verificationsRepository,
+ authorizationsRepository,
+ randomProvider,
+ usersRepository,
+ timeProvider,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/auth/domain/build.gradle.kts b/features/auth/domain/build.gradle.kts
new file mode 100644
index 00000000..10b1ddad
--- /dev/null
+++ b/features/auth/domain/build.gradle.kts
@@ -0,0 +1,23 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+}
+
+dependencies {
+ implementation(projects.foundation.time)
+ implementation(projects.foundation.pageToken)
+ implementation(projects.foundation.random)
+
+ implementation(projects.core.types.authIntegration)
+
+ api(projects.core.types)
+
+ testImplementation(libs.mockk)
+ testImplementation(libs.kotlin.test)
+ testImplementation(libs.kotlinx.coroutines.test)
+
+ testImplementation(projects.foundation.validation.testsIntegration)
+}
+
+tasks.withType {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/repositories/AuthorizationsRepository.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/AuthorizationsRepository.kt
similarity index 77%
rename from core/src/main/kotlin/io/timemates/backend/authorization/repositories/AuthorizationsRepository.kt
rename to features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/AuthorizationsRepository.kt
index 56acbabe..158e263f 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/repositories/AuthorizationsRepository.kt
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/AuthorizationsRepository.kt
@@ -1,20 +1,19 @@
-package io.timemates.backend.authorization.repositories
+package org.timemates.backend.auth.domain.repositories
import com.timemates.backend.time.UnixTime
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.AuthorizationId
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.common.markers.Repository
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.users.types.value.UserId
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.AuthorizationId
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.users.value.UserId
/**
* An interface that provides methods to manage user authorizations.
*/
-interface AuthorizationsRepository : Repository {
+interface AuthorizationsRepository {
/**
* Creates a new authorization for a user with the specified [userId], [accessToken], [refreshToken],
@@ -47,10 +46,10 @@ interface AuthorizationsRepository : Repository {
suspend fun remove(accessToken: AccessHash): Boolean
/**
- * Gets the authorization associated with the specified [accessToken], created after the specified [afterTime].
+ * Gets the authorization associated with the specified [accessToken].
*
* @param accessToken The access token used to authorize the user.
- * @param afterTime The time after which the authorization was created.
+ * @param currentTime The time of the request.
*
* @return The authorization associated with the specified [accessToken], or `null` if not found.
*/
diff --git a/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/EmailRepository.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/EmailRepository.kt
new file mode 100644
index 00000000..ee5acbcd
--- /dev/null
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/EmailRepository.kt
@@ -0,0 +1,11 @@
+package org.timemates.backend.auth.domain.repositories
+
+import org.timemates.backend.types.auth.Email
+import org.timemates.backend.types.users.value.EmailAddress
+
+interface EmailRepository {
+ /**
+ * Sends email to [emailAddress] with [email].
+ */
+ suspend fun send(emailAddress: EmailAddress, email: Email): Boolean
+}
\ No newline at end of file
diff --git a/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/UsersRepository.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/UsersRepository.kt
new file mode 100644
index 00000000..9f0d508a
--- /dev/null
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/UsersRepository.kt
@@ -0,0 +1,18 @@
+package org.timemates.backend.auth.domain.repositories
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.types.users.value.EmailAddress
+import org.timemates.backend.types.users.value.UserDescription
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.types.users.value.UserName
+
+interface UsersRepository {
+ suspend fun create(
+ emailAddress: EmailAddress,
+ userName: UserName,
+ userDescription: UserDescription?,
+ creationTime: UnixTime,
+ ): Result
+
+ suspend fun get(emailAddress: EmailAddress): Result
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/repositories/VerificationsRepository.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/VerificationsRepository.kt
similarity index 80%
rename from core/src/main/kotlin/io/timemates/backend/authorization/repositories/VerificationsRepository.kt
rename to features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/VerificationsRepository.kt
index df4d2332..53a3bd46 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/repositories/VerificationsRepository.kt
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/repositories/VerificationsRepository.kt
@@ -1,19 +1,18 @@
-package io.timemates.backend.authorization.repositories
+package org.timemates.backend.auth.domain.repositories
import com.timemates.backend.time.UnixTime
-import io.timemates.backend.authorization.types.Verification
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.value.Attempts
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.authorization.types.value.VerificationHash
-import io.timemates.backend.common.markers.Repository
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.users.types.value.EmailAddress
+import org.timemates.backend.types.auth.Verification
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.value.Attempts
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.auth.value.VerificationHash
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.users.value.EmailAddress
/**
* Repository that stores email verification codes.
*/
-interface VerificationsRepository : Repository {
+interface VerificationsRepository {
/**
* Saves a new email verification record with the specified [emailAddress], [verificationToken], [code],
@@ -32,14 +31,14 @@ interface VerificationsRepository : Repository {
time: UnixTime,
attempts: Attempts,
clientMetadata: ClientMetadata,
- )
+ ): Result
/**
* Adds an attempt to verify the email address associated with the specified [verificationToken].
*
* @param verificationToken The verification token used to confirm the email address.
*/
- suspend fun addAttempt(verificationToken: VerificationHash)
+ suspend fun addAttempt(verificationToken: VerificationHash): Result
/**
* Gets the verification record associated with the specified [verificationToken].
@@ -48,14 +47,14 @@ interface VerificationsRepository : Repository {
*
* @return The verification record associated with the specified [verificationToken], or `null` if not found.
*/
- suspend fun getVerification(verificationToken: VerificationHash): Verification?
+ suspend fun getVerification(verificationToken: VerificationHash): Result
/**
* Removes the verification record associated with the specified [verificationToken].
*
* @param verificationToken The verification token used to confirm the email address.
*/
- suspend fun remove(verificationToken: VerificationHash)
+ suspend fun remove(verificationToken: VerificationHash): Result
/**
* Gets the number of verification attempts made for the email address associated with the specified
@@ -69,7 +68,7 @@ interface VerificationsRepository : Repository {
*/
suspend fun getNumberOfAttempts(
emailAddress: EmailAddress, after: UnixTime,
- ): Count
+ ): Result
/**
* Gets the number of verification sessions created for the email address associated with the specified
@@ -84,7 +83,7 @@ interface VerificationsRepository : Repository {
suspend fun getNumberOfSessions(
emailAddress: EmailAddress,
after: UnixTime,
- ): Count
+ ): Result
/**
* Marks the verification associated with the specified [verificationToken] as confirmed.
diff --git a/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/AuthByEmailUseCase.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/AuthByEmailUseCase.kt
new file mode 100644
index 00000000..3321ec23
--- /dev/null
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/AuthByEmailUseCase.kt
@@ -0,0 +1,60 @@
+package org.timemates.backend.auth.domain.usecases
+
+import com.timemates.backend.time.TimeProvider
+import com.timemates.backend.time.UnixTime
+import com.timemates.random.RandomProvider
+import org.timemates.backend.auth.domain.repositories.EmailRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.types.auth.Email
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.value.Attempts
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.auth.value.VerificationHash
+import org.timemates.backend.types.users.value.EmailAddress
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+import kotlin.time.Duration.Companion.hours
+import kotlin.time.Duration.Companion.minutes
+
+class AuthByEmailUseCase(
+ private val emailRepository: EmailRepository,
+ private val verifications: VerificationsRepository,
+ private val timeProvider: TimeProvider,
+ private val randomProvider: RandomProvider,
+) {
+ @OptIn(ValidationDelicateApi::class)
+ suspend fun execute(emailAddress: EmailAddress, clientMetadata: ClientMetadata): Result {
+ // used for limits (max count of sessions & attempts that can be requested)
+ val sessionsTimeBoundary = timeProvider.provide() - 1.hours
+
+ val attempts = verifications.getNumberOfAttempts(emailAddress, sessionsTimeBoundary)
+ .getOrElse { throwable -> return Result.SendFailed(throwable) }
+ val sessionsCount = verifications.getNumberOfSessions(emailAddress, sessionsTimeBoundary)
+ .getOrElse { throwable -> return Result.SendFailed(throwable) }
+
+ return if (attempts.int >= 9 || sessionsCount.int >= 3) {
+ Result.AttemptsExceed
+ } else {
+ val code = VerificationCode.createUnsafe(randomProvider.randomHash(VerificationCode.SIZE))
+ val verificationHash = VerificationHash.createUnsafe(randomProvider.randomHash(VerificationHash.SIZE))
+ val expiresAt = timeProvider.provide() + 10.minutes
+ val totalAttempts = Attempts.createUnsafe(3)
+
+ if (!emailRepository.send(emailAddress, Email.AuthorizeEmail(emailAddress, code)))
+ return Result.SendFailed(null)
+ verifications.save(emailAddress, verificationHash, code, expiresAt, totalAttempts, clientMetadata)
+ Result.Success(verificationHash, timeProvider.provide() + 10.minutes, totalAttempts)
+ }
+ }
+
+ sealed interface Result {
+ data class SendFailed(val throwable: Throwable?) : Result
+ data class Success(
+ val verificationHash: VerificationHash,
+ val expiresAt: UnixTime,
+ val attempts: Attempts,
+ ) : Result
+
+ data object AttemptsExceed : Result
+ }
+}
\ No newline at end of file
diff --git a/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/ConfigureNewAccountUseCase.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/ConfigureNewAccountUseCase.kt
new file mode 100644
index 00000000..dcf9f443
--- /dev/null
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/ConfigureNewAccountUseCase.kt
@@ -0,0 +1,80 @@
+package org.timemates.backend.auth.domain.usecases
+
+import com.timemates.backend.time.TimeProvider
+import com.timemates.random.RandomProvider
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.repositories.UsersRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.foundation.authorization.Scope
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.auth.value.VerificationHash
+import org.timemates.backend.types.users.value.UserDescription
+import org.timemates.backend.types.users.value.UserName
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+import kotlin.time.Duration.Companion.days
+
+class ConfigureNewAccountUseCase(
+ private val usersRepository: UsersRepository,
+ private val authorizations: AuthorizationsRepository,
+ private val verifications: VerificationsRepository,
+ private val timeProvider: TimeProvider,
+ private val randomProvider: RandomProvider,
+) {
+ @OptIn(ValidationDelicateApi::class)
+ suspend fun execute(
+ verificationToken: VerificationHash,
+ userName: UserName,
+ shortBio: UserDescription?,
+ ): Result {
+ val verification = verifications.getVerification(verificationToken)
+ .getOrElse { throwable -> return Result.Failure(throwable) }
+ ?.takeIf { it.isConfirmed }
+ ?: return Result.NotFound
+
+ val currentTime = timeProvider.provide()
+
+ val accessHash = AccessHash.createUnsafe(randomProvider.randomHash(AccessHash.SIZE))
+ val refreshHash = RefreshHash.createUnsafe(randomProvider.randomHash(RefreshHash.SIZE))
+ val expiresAt = currentTime + 30.days
+ val metadata = verification.clientMetadata
+
+ val id = usersRepository.create(
+ emailAddress = verification.emailAddress,
+ userName = userName,
+ userDescription = shortBio,
+ creationTime = timeProvider.provide()
+ ).getOrElse { throwable -> return Result.Failure(throwable) }
+
+ authorizations.create(
+ userId = id,
+ accessToken = accessHash,
+ refreshToken = refreshHash,
+ expiresAt = expiresAt,
+ creationTime = currentTime,
+ clientMetadata = metadata,
+ )
+
+ verifications.remove(verificationToken)
+
+ return Result.Success(
+ Authorization(
+ userId = id,
+ accessHash = accessHash,
+ refreshAccessHash = refreshHash,
+ scopes = listOf(Scope.All),
+ expiresAt = expiresAt,
+ createdAt = currentTime,
+ clientMetadata = metadata,
+ )
+ )
+ }
+
+ sealed class Result {
+ data object NotFound : Result()
+ data class Success(val authorization: Authorization) : Result()
+ data class Failure(val throwable: Throwable) : Result()
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetAuthorizationUseCase.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetAuthorizationUseCase.kt
similarity index 63%
rename from core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetAuthorizationUseCase.kt
rename to features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetAuthorizationUseCase.kt
index 1ceb7509..ebce779f 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetAuthorizationUseCase.kt
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetAuthorizationUseCase.kt
@@ -1,15 +1,14 @@
-package io.timemates.backend.authorization.usecases
+package org.timemates.backend.auth.domain.usecases
import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.common.markers.UseCase
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.value.AccessHash
class GetAuthorizationUseCase(
private val authorizationsRepository: AuthorizationsRepository,
private val timerProvider: TimeProvider,
-) : UseCase {
+) {
suspend fun execute(accessHash: AccessHash): Result {
return authorizationsRepository.get(accessHash, timerProvider.provide())
?.let { Result.Success(it) }
diff --git a/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetAuthorizationsUseCase.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetAuthorizationsUseCase.kt
new file mode 100644
index 00000000..2b493e11
--- /dev/null
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetAuthorizationsUseCase.kt
@@ -0,0 +1,24 @@
+package org.timemates.backend.auth.domain.usecases
+
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.AuthorizationsScope
+import org.timemates.backend.types.auth.exceptions.AuthorizationException
+
+class GetAuthorizationsUseCase(
+ private val authorizationsRepository: AuthorizationsRepository,
+) {
+ suspend fun execute(auth: Authorized, pageToken: PageToken?): Result {
+ val result = authorizationsRepository.getList(auth.userId, pageToken)
+ return Result.Success(result.value, result.nextPageToken)
+ }
+
+ sealed class Result {
+ data class Success(val list: List, val nextPageToken: PageToken?) : Result()
+
+ data class AuthorizationFailure(val exception: AuthorizationException) : Result()
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetUserIdByAccessTokenUseCase.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetUserIdByAccessTokenUseCase.kt
similarity index 62%
rename from core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetUserIdByAccessTokenUseCase.kt
rename to features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetUserIdByAccessTokenUseCase.kt
index ed5c3d66..160b52d2 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/GetUserIdByAccessTokenUseCase.kt
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/GetUserIdByAccessTokenUseCase.kt
@@ -1,15 +1,14 @@
-package io.timemates.backend.authorization.usecases
+package org.timemates.backend.auth.domain.usecases
import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.users.types.value.UserId
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.users.value.UserId
class GetUserIdByAccessTokenUseCase(
private val authorizations: AuthorizationsRepository,
private val time: TimeProvider,
-) : UseCase {
+) {
suspend fun execute(accessHash: AccessHash): Result {
val auth = authorizations.get(accessHash, time.provide()) ?: return Result.NotFound
return Result.Success(auth.userId)
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/RefreshTokenUseCase.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/RefreshAccessTokenUseCase.kt
similarity index 52%
rename from core/src/main/kotlin/io/timemates/backend/authorization/usecases/RefreshTokenUseCase.kt
rename to features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/RefreshAccessTokenUseCase.kt
index e94dc66c..2f4b730e 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/RefreshTokenUseCase.kt
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/RefreshAccessTokenUseCase.kt
@@ -1,28 +1,29 @@
-package io.timemates.backend.authorization.usecases
+package org.timemates.backend.auth.domain.usecases
import com.timemates.backend.time.TimeProvider
import com.timemates.random.RandomProvider
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.validation.createOrThrowInternally
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
import kotlin.time.Duration.Companion.days
-class RefreshTokenUseCase(
+class RefreshAccessTokenUseCase(
private val randomProvider: RandomProvider,
private val authorizations: AuthorizationsRepository,
private val time: TimeProvider,
-) : UseCase {
+) {
+ @OptIn(ValidationDelicateApi::class)
suspend fun execute(
refreshToken: RefreshHash,
): Result {
return Result.Success(
authorizations.renew(
refreshToken,
- AccessHash.createOrThrowInternally(randomProvider.randomHash(AccessHash.SIZE)),
- time.provide() + 30.days
+ AccessHash.createUnsafe(randomProvider.randomHash(AccessHash.SIZE)),
+ time.provide() + 30.days,
) ?: return Result.InvalidAuthorization
)
}
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/RemoveAccessTokenUseCase.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/RemoveAccessTokenUseCase.kt
similarity index 60%
rename from core/src/main/kotlin/io/timemates/backend/authorization/usecases/RemoveAccessTokenUseCase.kt
rename to features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/RemoveAccessTokenUseCase.kt
index 8de5509f..e5cbc0a0 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/RemoveAccessTokenUseCase.kt
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/RemoveAccessTokenUseCase.kt
@@ -1,12 +1,11 @@
-package io.timemates.backend.authorization.usecases
+package org.timemates.backend.auth.domain.usecases
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.common.markers.UseCase
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.types.auth.value.AccessHash
class RemoveAccessTokenUseCase(
private val tokens: AuthorizationsRepository,
-) : UseCase {
+) {
suspend fun execute(
accessHash: AccessHash,
): Result {
diff --git a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/VerifyAuthorizationUseCase.kt b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/VerifyAuthorizationUseCase.kt
similarity index 67%
rename from core/src/main/kotlin/io/timemates/backend/authorization/usecases/VerifyAuthorizationUseCase.kt
rename to features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/VerifyAuthorizationUseCase.kt
index 094fd06d..320aa546 100644
--- a/core/src/main/kotlin/io/timemates/backend/authorization/usecases/VerifyAuthorizationUseCase.kt
+++ b/features/auth/domain/src/main/kotlin/org/timemates/backend/auth/domain/usecases/VerifyAuthorizationUseCase.kt
@@ -1,46 +1,49 @@
-package io.timemates.backend.authorization.usecases
+package org.timemates.backend.auth.domain.usecases
import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.validation.createOrThrowInternally
import com.timemates.random.RandomProvider
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.repositories.VerificationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.authorization.types.value.VerificationCode
-import io.timemates.backend.authorization.types.value.VerificationHash
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.Scope
-import io.timemates.backend.users.repositories.UsersRepository
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.repositories.UsersRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.foundation.authorization.Scope
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.auth.value.VerificationHash
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
import kotlin.time.Duration.Companion.days
class VerifyAuthorizationUseCase(
private val verifications: VerificationsRepository,
private val authorizations: AuthorizationsRepository,
private val randomProvider: RandomProvider,
- private val users: UsersRepository,
+ private val usersRepository: UsersRepository,
private val timeProvider: TimeProvider,
-) : UseCase {
+) {
+ @OptIn(ValidationDelicateApi::class)
suspend fun execute(
verificationToken: VerificationHash,
code: VerificationCode,
): Result {
val verification = verifications.getVerification(verificationToken)
+ .getOrElse { throwable -> return Result.Failure(throwable) }
?: return Result.NotFound
return when (verification.attempts.int) {
in 1..3 -> {
if (verification.code == code) {
- val accessToken = AccessHash.createOrThrowInternally(
+ val accessToken = AccessHash.createUnsafe(
randomProvider.randomHash(AccessHash.SIZE)
)
- val refreshToken = RefreshHash.createOrThrowInternally(
+ val refreshToken = RefreshHash.createUnsafe(
randomProvider.randomHash(AccessHash.SIZE)
)
val metadata = verification.clientMetadata
- val userId = users.getUserIdByEmail(verification.emailAddress)
+ val userId = usersRepository.get(verification.emailAddress)
+ .getOrElse { throwable -> return Result.Failure(throwable) }
return if (userId != null) {
val expireTime = timeProvider.provide() + 30.days
@@ -91,6 +94,8 @@ class VerifyAuthorizationUseCase(
data object AttemptsExceed : Result()
data object AttemptFailed : Result()
+ data class Failure(val throwable: Throwable) : Result()
+
data object NotFound : Result()
}
}
\ No newline at end of file
diff --git a/core/src/test/kotlin/io/timemates/backend/auth/usecases/AuthByEmailUseCaseTest.kt b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/AuthByEmailUseCaseTest.kt
similarity index 56%
rename from core/src/test/kotlin/io/timemates/backend/auth/usecases/AuthByEmailUseCaseTest.kt
rename to features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/AuthByEmailUseCaseTest.kt
index 80ad5bc1..9a74d0b6 100644
--- a/core/src/test/kotlin/io/timemates/backend/auth/usecases/AuthByEmailUseCaseTest.kt
+++ b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/AuthByEmailUseCaseTest.kt
@@ -1,31 +1,34 @@
-package io.timemates.backend.auth.usecases
+package org.timemates.backend.auth.usecases
import com.timemates.backend.time.TimeProvider
import com.timemates.backend.time.UnixTime
import com.timemates.random.RandomProvider
-import com.timemates.random.SecureRandomProvider
import io.mockk.coEvery
import io.mockk.coJustRun
import io.mockk.every
import io.mockk.impl.annotations.MockK
import io.mockk.junit5.MockKExtension
-import io.mockk.spyk
-import io.timemates.backend.authorization.repositories.VerificationsRepository
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.metadata.value.ClientIpAddress
-import io.timemates.backend.authorization.types.metadata.value.ClientName
-import io.timemates.backend.authorization.types.metadata.value.ClientVersion
-import io.timemates.backend.authorization.usecases.AuthByEmailUseCase
-import io.timemates.backend.common.repositories.EmailsRepository
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.testing.validation.createOrAssert
-import io.timemates.backend.users.types.value.EmailAddress
-import kotlinx.coroutines.runBlocking
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.TestInstance
import org.junit.jupiter.api.extension.ExtendWith
import org.junit.platform.commons.annotation.Testable
+import org.timemates.backend.auth.domain.repositories.EmailRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.auth.domain.usecases.AuthByEmailUseCase
+import org.timemates.backend.foundation.validation.test.createOrAssert
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.metadata.value.ClientIpAddress
+import org.timemates.backend.types.auth.metadata.value.ClientName
+import org.timemates.backend.types.auth.metadata.value.ClientVersion
+import org.timemates.backend.types.auth.value.Attempts
+import org.timemates.backend.types.auth.value.VerificationHash
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.users.value.EmailAddress
+import kotlin.test.Test
import kotlin.test.assertEquals
+import kotlin.time.Duration.Companion.minutes
@Testable
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
@@ -38,9 +41,9 @@ class AuthByEmailUseCaseTest {
lateinit var timeProvider: TimeProvider
@MockK
- lateinit var emailsRepository: EmailsRepository
+ lateinit var emailsRepository: EmailRepository
- private val randomProvider: RandomProvider = SecureRandomProvider()
+ private val randomProvider: RandomProvider = mockk()
private lateinit var useCase: AuthByEmailUseCase
@@ -53,40 +56,46 @@ class AuthByEmailUseCaseTest {
)
}
- //@Test
- fun `test success email sending`(): Unit = runBlocking {
+ @Test
+ fun `test success email sending`(): Unit = runTest {
// GIVEN
- val clientMetadata = spyk()
+ val clientMetadata = mockk()
val time = UnixTime.createOrAssert(System.currentTimeMillis())
- coEvery { verificationsRepository.getNumberOfAttempts(any(), any()) } returns Count.createOrAssert(0)
- coEvery { verificationsRepository.getNumberOfSessions(any(), any()) } returns Count.createOrAssert(0)
+ val hash = "A".repeat(128)
+ val code = "12345678"
+
+ coEvery { verificationsRepository.getNumberOfAttempts(any(), any()) } returns Result.success(Count.createOrAssert(0))
+ coEvery { verificationsRepository.getNumberOfSessions(any(), any()) } returns Result.success(Count.createOrAssert(0))
every { timeProvider.provide() } returns time
coEvery { emailsRepository.send(any(), any()) } returns true
coJustRun { verificationsRepository.save(any(), any(), any(), any(), any(), any()) }
+ every { randomProvider.randomHash(eq(128)) } returns hash
+ every { randomProvider.randomHash(eq(8)) } returns code
// WHEN
val result = useCase.execute(email, clientMetadata)
// THEN
-// assertEquals(
-// expected = AuthByEmailUseCase.Result.Success(
-// time + 10.minutes,
-// Attempts.createOrAssert(3)
-// ),
-// actual = result
-// )
+ assertEquals(
+ expected = AuthByEmailUseCase.Result.Success(
+ VerificationHash.createOrAssert(hash),
+ time + 10.minutes,
+ Attempts.createOrAssert(3)
+ ),
+ actual = result
+ )
}
- // @Test
- fun `test sessions number exceed`(): Unit = runBlocking {
+ @Test
+ fun `test sessions number exceed`(): Unit = runTest {
// GIVEN
val clientMetadata = ClientMetadata(
clientName = ClientName.createOrAssert("name"),
clientVersion = ClientVersion.createOrAssert(1.0),
clientIpAddress = ClientIpAddress.createOrAssert("ip_address"),
)
- coEvery { verificationsRepository.getNumberOfAttempts(any(), any()) } returns Count.createOrAssert(0)
- coEvery { verificationsRepository.getNumberOfSessions(any(), any()) } returns Count.createOrAssert(3)
+ coEvery { verificationsRepository.getNumberOfAttempts(any(), any()) } returns Result.success(Count.createOrAssert(0))
+ coEvery { verificationsRepository.getNumberOfSessions(any(), any()) } returns Result.success(Count.createOrAssert(3))
every { timeProvider.provide() } returns UnixTime.createOrAssert(System.currentTimeMillis())
coJustRun { verificationsRepository.save(any(), any(), any(), any(), any(), any()) }
@@ -100,11 +109,12 @@ class AuthByEmailUseCaseTest {
)
}
- // @Test
- fun `test attempts number exceed`(): Unit = runBlocking {
+ @Test
+ fun `test attempts number exceed`(): Unit = runTest {
// GIVEN
- val clientMetadata = spyk()
- coEvery { verificationsRepository.getNumberOfAttempts(any(), any()) } returns Count.createOrAssert(9)
+ val clientMetadata = mockk()
+ coEvery { verificationsRepository.getNumberOfSessions(any(), any()) } returns Result.success(Count.createOrAssert(1))
+ coEvery { verificationsRepository.getNumberOfAttempts(any(), any()) } returns Result.success(Count.createOrAssert(9))
every { timeProvider.provide() } returns UnixTime.createOrAssert(System.currentTimeMillis())
coJustRun { verificationsRepository.save(any(), any(), any(), any(), any(), any()) }
diff --git a/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/ConfigureNewAccountUseCaseTest.kt b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/ConfigureNewAccountUseCaseTest.kt
new file mode 100644
index 00000000..f883aea4
--- /dev/null
+++ b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/ConfigureNewAccountUseCaseTest.kt
@@ -0,0 +1,126 @@
+package org.timemates.backend.auth.usecases
+
+import com.timemates.backend.time.SystemTimeProvider
+import com.timemates.backend.time.TimeProvider
+import com.timemates.random.SecureRandomProvider
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import io.mockk.junit5.MockKExtension
+import io.mockk.mockk
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.BeforeAll
+import org.junit.jupiter.api.TestInstance
+import org.junit.jupiter.api.extension.ExtendWith
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.repositories.UsersRepository
+import org.timemates.backend.auth.domain.repositories.VerificationsRepository
+import org.timemates.backend.auth.domain.usecases.ConfigureNewAccountUseCase
+import org.timemates.backend.foundation.validation.test.createOrAssert
+import org.timemates.backend.types.auth.Verification
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.value.Attempts
+import org.timemates.backend.types.auth.value.AuthorizationId
+import org.timemates.backend.types.auth.value.VerificationCode
+import org.timemates.backend.types.auth.value.VerificationHash
+import org.timemates.backend.types.users.value.EmailAddress
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.types.users.value.UserName
+import kotlin.test.Test
+import kotlin.test.assertIs
+
+@MockKExtension.CheckUnnecessaryStub
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+@ExtendWith(MockKExtension::class)
+class ConfigureNewAccountUseCaseTest {
+ @MockK
+ lateinit var verificationsRepository: VerificationsRepository
+
+ @MockK
+ lateinit var authorizationsRepository: AuthorizationsRepository
+
+ @MockK
+ lateinit var usersRepository: UsersRepository
+
+ private val timeProvider: TimeProvider = SystemTimeProvider()
+
+ private val randomProvider = SecureRandomProvider()
+
+ private lateinit var useCase: ConfigureNewAccountUseCase
+
+ @BeforeAll
+ fun before() {
+ useCase = ConfigureNewAccountUseCase(
+ usersRepository,
+ authorizationsRepository,
+ verificationsRepository,
+ timeProvider,
+ randomProvider
+ )
+ }
+
+ @Test
+ fun `check configure new account when everything is okay`(): Unit = runTest {
+ // GIVEN
+ val clientMetadata = mockk()
+ val userId = mockk(relaxed = true)
+ val email = EmailAddress.createOrAssert("test@email.com")
+ val verificationHash = VerificationHash.createOrAssert(randomProvider.randomHash(
+ VerificationHash.SIZE))
+ coEvery { verificationsRepository.getVerification(any()) }
+ .returns(
+ Result.success(
+ Verification(
+ email,
+ VerificationCode.createOrAssert("1234F321"),
+ Attempts.createOrAssert(3),
+ timeProvider.provide(),
+ true,
+ clientMetadata,
+ )
+ )
+ )
+ coEvery { verificationsRepository.remove(any()) } returns (Result.success(Unit))
+ coEvery { authorizationsRepository.create(any(), any(), any(), any(), any(), any()) }
+ .returns(AuthorizationId.createOrAssert(0))
+ coEvery { usersRepository.create(any(), any(), any(), any()) }
+ .returns(Result.success(userId))
+
+ // WHEN
+ val result = useCase.execute(
+ verificationHash,
+ UserName.createOrAssert("Test"),
+ null
+ )
+
+ // THEN
+ assertIs(value = result)
+ }
+
+ @Test
+ fun `check configure returns not found when verification is not found in repo`(): Unit = runTest {
+ // WHEN
+ coEvery { verificationsRepository.getVerification(any()) } returns Result.success(null)
+
+ // should fail as there is no such token
+ assertIs(
+ useCase.execute(
+ mockk(relaxed = true),
+ UserName.createOrAssert("test"), null
+ )
+ )
+ }
+
+ @Test
+ fun `check configure returns not found when exception in repo`(): Unit = runTest {
+ // WHEN
+ coEvery { verificationsRepository.getVerification(any()) } returns Result.failure(mockk())
+
+ // should fail as there is no such token
+ assertIs(
+ useCase.execute(
+ mockk(relaxed = true),
+ UserName.createOrAssert("test"), null
+ )
+ )
+ }
+}
\ No newline at end of file
diff --git a/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetAuthorizationUseCaseTest.kt b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetAuthorizationUseCaseTest.kt
similarity index 68%
rename from core/src/test/kotlin/io/timemates/backend/auth/usecases/GetAuthorizationUseCaseTest.kt
rename to features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetAuthorizationUseCaseTest.kt
index 28245f39..5b5a6c35 100644
--- a/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetAuthorizationUseCaseTest.kt
+++ b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetAuthorizationUseCaseTest.kt
@@ -1,27 +1,26 @@
-package io.timemates.backend.auth.usecases
+package org.timemates.backend.auth.usecases
import com.timemates.backend.time.SystemTimeProvider
import com.timemates.backend.time.UnixTime
import com.timemates.random.SecureRandomProvider
-import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.mockk
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.metadata.value.ClientIpAddress
-import io.timemates.backend.authorization.types.metadata.value.ClientName
-import io.timemates.backend.authorization.types.metadata.value.ClientVersion
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.authorization.usecases.GetAuthorizationUseCase
-import io.timemates.backend.testing.validation.createOrAssert
-import io.timemates.backend.users.types.value.UserId
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.platform.commons.annotation.Testable
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.GetAuthorizationUseCase
+import org.timemates.backend.foundation.validation.test.createOrAssert
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.metadata.value.ClientIpAddress
+import org.timemates.backend.types.auth.metadata.value.ClientName
+import org.timemates.backend.types.auth.metadata.value.ClientVersion
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.users.value.UserId
import kotlin.test.assertEquals
@Testable
@@ -36,7 +35,6 @@ class GetAuthorizationUseCaseTest {
@BeforeAll
fun before() {
- MockKAnnotations.init(this)
useCase = GetAuthorizationUseCase(
authorizationsRepository = authorizationsRepository,
timerProvider = timeProvider
@@ -44,11 +42,12 @@ class GetAuthorizationUseCaseTest {
}
@Test
- fun `successful authorization should pass`() = runBlocking {
+ fun `successful authorization should pass`() = runTest {
// GIVEN
val accessHashValue = randomProvider.randomHash(AccessHash.SIZE)
val accessHash = AccessHash.createOrAssert(accessHashValue)
- val refreshHash = RefreshHash.createOrAssert(randomProvider.randomHash(AccessHash.SIZE))
+ val refreshHash = RefreshHash.createOrAssert(randomProvider.randomHash(
+ AccessHash.SIZE))
val authorization = Authorization(
userId = UserId.createOrAssert(1235),
accessHash = accessHash,
@@ -70,7 +69,7 @@ class GetAuthorizationUseCaseTest {
}
@Test
- fun `invalid authorization should return NotFound`() = runBlocking {
+ fun `invalid authorization should return NotFound`() = runTest {
// GIVEN
val accessHash = AccessHash.createOrAssert(randomProvider.randomHash(AccessHash.SIZE))
coEvery { authorizationsRepository.get(any(), any()) }.returns(null)
diff --git a/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetAuthorizationsUseCaseTest.kt b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetAuthorizationsUseCaseTest.kt
new file mode 100644
index 00000000..a557a7f8
--- /dev/null
+++ b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetAuthorizationsUseCaseTest.kt
@@ -0,0 +1,54 @@
+package org.timemates.backend.auth.usecases
+
+import com.timemates.random.SecureRandomProvider
+import io.mockk.MockKAnnotations
+import io.mockk.coEvery
+import io.mockk.impl.annotations.MockK
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.TestInstance
+import org.junit.platform.commons.annotation.Testable
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.GetAuthorizationsUseCase
+import org.timemates.backend.foundation.authorization.AuthDelicateApi
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.foundation.authorization.types.AuthorizedId
+import org.timemates.backend.pagination.Ordering
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import kotlin.test.Test
+import kotlin.test.assertEquals
+
+
+@OptIn(AuthDelicateApi::class)
+@Testable
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class GetAuthorizationsUseCaseTest {
+
+ private lateinit var useCase: GetAuthorizationsUseCase
+ private val randomProvider = SecureRandomProvider()
+
+ @MockK
+ private lateinit var authorizationsRepository: AuthorizationsRepository
+
+ private val authorizedId = AuthorizedId(0)
+
+ @BeforeEach
+ fun before() {
+ MockKAnnotations.init(this)
+ useCase = GetAuthorizationsUseCase(
+ authorizationsRepository = authorizationsRepository
+ )
+ }
+
+ @Test
+ fun `test success get authorizations`() = runTest {
+ val pageToken = PageToken.toGive(randomProvider.randomHash(64))
+ val nextPageToken = PageToken.toGive(randomProvider.randomHash(64))
+ coEvery { authorizationsRepository.getList(any(), any()) }.returns(
+ Page(emptyList(), nextPageToken, Ordering.ASCENDING)
+ )
+ val result: GetAuthorizationsUseCase.Result = useCase.execute(Authorized(authorizedId), pageToken)
+ assertEquals(GetAuthorizationsUseCase.Result.Success(emptyList(), nextPageToken), result)
+ }
+}
\ No newline at end of file
diff --git a/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetUserIdByAccessTokenUseCaseTest.kt b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetUserIdByAccessTokenUseCaseTest.kt
similarity index 70%
rename from core/src/test/kotlin/io/timemates/backend/auth/usecases/GetUserIdByAccessTokenUseCaseTest.kt
rename to features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetUserIdByAccessTokenUseCaseTest.kt
index fc0a47fd..aa422b61 100644
--- a/core/src/test/kotlin/io/timemates/backend/auth/usecases/GetUserIdByAccessTokenUseCaseTest.kt
+++ b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/GetUserIdByAccessTokenUseCaseTest.kt
@@ -1,27 +1,26 @@
-package io.timemates.backend.auth.usecases
+package org.timemates.backend.auth.usecases
import com.timemates.backend.time.SystemTimeProvider
import com.timemates.backend.time.UnixTime
import com.timemates.random.SecureRandomProvider
-import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.mockk
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.metadata.value.ClientIpAddress
-import io.timemates.backend.authorization.types.metadata.value.ClientName
-import io.timemates.backend.authorization.types.metadata.value.ClientVersion
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.authorization.usecases.GetUserIdByAccessTokenUseCase
-import io.timemates.backend.testing.validation.createOrAssert
-import io.timemates.backend.users.types.value.UserId
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.platform.commons.annotation.Testable
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.GetUserIdByAccessTokenUseCase
+import org.timemates.backend.foundation.validation.test.createOrAssert
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.metadata.value.ClientIpAddress
+import org.timemates.backend.types.auth.metadata.value.ClientName
+import org.timemates.backend.types.auth.metadata.value.ClientVersion
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.users.value.UserId
import kotlin.test.assertEquals
@Testable
@@ -36,7 +35,6 @@ class GetUserIdByAccessTokenUseCaseTest {
@BeforeAll
fun before() {
- MockKAnnotations.init(this)
useCase = GetUserIdByAccessTokenUseCase(
authorizations = authorizationsRepository,
time = timeProvider
@@ -44,10 +42,11 @@ class GetUserIdByAccessTokenUseCaseTest {
}
@Test
- fun `get user id by valid access hash should pass`() = runBlocking {
+ fun `get user id by valid access hash should pass`() = runTest {
// GIVEN
val accessHash = AccessHash.createOrAssert(randomProvider.randomHash(AccessHash.SIZE))
- val refreshHash = RefreshHash.createOrAssert(randomProvider.randomHash(RefreshHash.SIZE))
+ val refreshHash = RefreshHash.createOrAssert(randomProvider.randomHash(
+ RefreshHash.SIZE))
val time = UnixTime.createOrAssert(123232335)
val userId = UserId.createOrAssert(1235)
val authorization = Authorization(
@@ -71,7 +70,7 @@ class GetUserIdByAccessTokenUseCaseTest {
}
@Test
- fun `get user id by invalid access hash should return NotFound`() = runBlocking {
+ fun `get user id by invalid access hash should return NotFound`() = runTest {
// GIVEN
val accessHash = AccessHash.createOrAssert(randomProvider.randomHash(AccessHash.SIZE))
coEvery { authorizationsRepository.get(any(), any()) }.returns(null)
diff --git a/core/src/test/kotlin/io/timemates/backend/auth/usecases/RefreshTokenUseCaseTest.kt b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/RefreshTokenUseCaseTest.kt
similarity index 63%
rename from core/src/test/kotlin/io/timemates/backend/auth/usecases/RefreshTokenUseCaseTest.kt
rename to features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/RefreshTokenUseCaseTest.kt
index 53fd20be..82ccbbe4 100644
--- a/core/src/test/kotlin/io/timemates/backend/auth/usecases/RefreshTokenUseCaseTest.kt
+++ b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/RefreshTokenUseCaseTest.kt
@@ -1,34 +1,33 @@
-package io.timemates.backend.auth.usecases
+package org.timemates.backend.auth.usecases
import com.timemates.backend.time.SystemTimeProvider
import com.timemates.backend.time.UnixTime
import com.timemates.random.SecureRandomProvider
-import io.mockk.MockKAnnotations
import io.mockk.coEvery
import io.mockk.mockk
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.Authorization
-import io.timemates.backend.authorization.types.metadata.ClientMetadata
-import io.timemates.backend.authorization.types.metadata.value.ClientIpAddress
-import io.timemates.backend.authorization.types.metadata.value.ClientName
-import io.timemates.backend.authorization.types.metadata.value.ClientVersion
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.types.value.RefreshHash
-import io.timemates.backend.authorization.usecases.RefreshTokenUseCase
-import io.timemates.backend.testing.validation.createOrAssert
-import io.timemates.backend.users.types.value.UserId
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeAll
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.platform.commons.annotation.Testable
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.RefreshAccessTokenUseCase
+import org.timemates.backend.foundation.validation.test.createOrAssert
+import org.timemates.backend.types.auth.Authorization
+import org.timemates.backend.types.auth.metadata.ClientMetadata
+import org.timemates.backend.types.auth.metadata.value.ClientIpAddress
+import org.timemates.backend.types.auth.metadata.value.ClientName
+import org.timemates.backend.types.auth.metadata.value.ClientVersion
+import org.timemates.backend.types.auth.value.AccessHash
+import org.timemates.backend.types.auth.value.RefreshHash
+import org.timemates.backend.types.users.value.UserId
import kotlin.test.assertEquals
@Testable
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class RefreshTokenUseCaseTest {
- private lateinit var useCase: RefreshTokenUseCase
+ private lateinit var useCase: RefreshAccessTokenUseCase
private val randomProvider = SecureRandomProvider()
private val timeProvider = SystemTimeProvider()
@@ -36,8 +35,7 @@ class RefreshTokenUseCaseTest {
@BeforeAll
fun before() {
- MockKAnnotations.init(this)
- useCase = RefreshTokenUseCase(
+ useCase = RefreshAccessTokenUseCase(
randomProvider = randomProvider,
authorizations = authorizationsRepository,
time = timeProvider
@@ -45,11 +43,12 @@ class RefreshTokenUseCaseTest {
}
@Test
- fun `refresh access token by valid access hash should pass`() = runBlocking {
+ fun `refresh access token by valid access hash should pass`() = runTest {
// GIVEN
val accessHashValue = randomProvider.randomHash(AccessHash.SIZE)
val accessHash = AccessHash.createOrAssert(accessHashValue)
- val refreshHash = RefreshHash.createOrAssert(randomProvider.randomHash(AccessHash.SIZE))
+ val refreshHash = RefreshHash.createOrAssert(randomProvider.randomHash(
+ AccessHash.SIZE))
val authorization = Authorization(
userId = UserId.createOrAssert(1235),
accessHash = accessHash,
@@ -67,17 +66,18 @@ class RefreshTokenUseCaseTest {
// WHEN
val result = useCase.execute(refreshHash)
// THEN
- assertEquals(RefreshTokenUseCase.Result.Success(authorization), result)
+ assertEquals(RefreshAccessTokenUseCase.Result.Success(authorization), result)
}
@Test
- fun `refresh access token by invalid access hash should return InvalidAuthorization`() = runBlocking {
+ fun `refresh access token by invalid access hash should return InvalidAuthorization`() = runTest {
// GIVEN
- val refreshHash = RefreshHash.createOrAssert(randomProvider.randomHash(AccessHash.SIZE))
+ val refreshHash = RefreshHash.createOrAssert(randomProvider.randomHash(
+ AccessHash.SIZE))
coEvery { authorizationsRepository.renew(any(), any(), any()) }.returns(null)
// WHEN
val result = useCase.execute(refreshHash)
// THEN
- assertEquals(RefreshTokenUseCase.Result.InvalidAuthorization, result)
+ assertEquals(RefreshAccessTokenUseCase.Result.InvalidAuthorization, result)
}
}
\ No newline at end of file
diff --git a/core/src/test/kotlin/io/timemates/backend/auth/usecases/RemoveAccessTokenUseCaseTest.kt b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/RemoveAccessTokenUseCaseTest.kt
similarity index 73%
rename from core/src/test/kotlin/io/timemates/backend/auth/usecases/RemoveAccessTokenUseCaseTest.kt
rename to features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/RemoveAccessTokenUseCaseTest.kt
index 2257a79f..2393018b 100644
--- a/core/src/test/kotlin/io/timemates/backend/auth/usecases/RemoveAccessTokenUseCaseTest.kt
+++ b/features/auth/domain/src/test/kotlin/org/timemates/backend/auth/usecases/RemoveAccessTokenUseCaseTest.kt
@@ -1,20 +1,17 @@
-package io.timemates.backend.auth.usecases
+package org.timemates.backend.auth.usecases
import com.timemates.random.SecureRandomProvider
-import io.mockk.MockKAnnotations
import io.mockk.coEvery
-import io.mockk.impl.annotations.MockK
import io.mockk.mockk
-import io.timemates.backend.authorization.repositories.AuthorizationsRepository
-import io.timemates.backend.authorization.types.value.AccessHash
-import io.timemates.backend.authorization.usecases.RemoveAccessTokenUseCase
-import io.timemates.backend.testing.validation.createOrAssert
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.BeforeAll
-import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.TestInstance
import org.junit.platform.commons.annotation.Testable
+import org.timemates.backend.auth.domain.repositories.AuthorizationsRepository
+import org.timemates.backend.auth.domain.usecases.RemoveAccessTokenUseCase
+import org.timemates.backend.foundation.validation.test.createOrAssert
+import org.timemates.backend.types.auth.value.AccessHash
import kotlin.test.assertEquals
@Testable
@@ -28,12 +25,11 @@ class RemoveAccessTokenUseCaseTest {
@BeforeAll
fun before() {
- MockKAnnotations.init(this)
useCase = RemoveAccessTokenUseCase(authorizationsRepository)
}
@Test
- fun `remove current valid authorization should pass`() = runBlocking {
+ fun `remove current valid authorization should pass`() = runTest {
// GIVEN
val accessHash = AccessHash.createOrAssert(randomProvider.randomHash(AccessHash.SIZE))
coEvery { authorizationsRepository.remove(accessHash) }.returns(true)
@@ -44,7 +40,7 @@ class RemoveAccessTokenUseCaseTest {
}
@Test
- fun `remove current authorization by invalid access hash should fail`() = runBlocking {
+ fun `remove current authorization by invalid access hash should fail`() = runTest {
// GIVEN
val accessHash = AccessHash.createOrAssert(randomProvider.randomHash(AccessHash.SIZE))
coEvery { authorizationsRepository.remove(any()) }.returns(false)
diff --git a/features/timers/adapters/build.gradle.kts b/features/timers/adapters/build.gradle.kts
new file mode 100644
index 00000000..a9a2e411
--- /dev/null
+++ b/features/timers/adapters/build.gradle.kts
@@ -0,0 +1,9 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+dependencies {
+ implementation(projects.features.timers.domain)
+ implementation(projects.features.users.domain)
+}
\ No newline at end of file
diff --git a/features/timers/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/UsersRepositoryAdapter.kt b/features/timers/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/UsersRepositoryAdapter.kt
new file mode 100644
index 00000000..fe3c7d0b
--- /dev/null
+++ b/features/timers/adapters/src/main/kotlin/org/timemates/backend/auth/adapters/UsersRepositoryAdapter.kt
@@ -0,0 +1,14 @@
+package org.timemates.backend.auth.adapters
+
+import org.timemates.backend.timers.domain.repositories.UsersRepository
+import org.timemates.backend.types.users.User
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.users.domain.repositories.UsersRepository as UsersRepositoryDelegate
+
+class UsersRepositoryAdapter(
+ private val delegate: UsersRepositoryDelegate,
+) : UsersRepository {
+ override suspend fun getUsers(userIds: List): List {
+ return delegate.getUsers(userIds)
+ }
+}
\ No newline at end of file
diff --git a/features/timers/data/build.gradle.kts b/features/timers/data/build.gradle.kts
new file mode 100644
index 00000000..c99b45e9
--- /dev/null
+++ b/features/timers/data/build.gradle.kts
@@ -0,0 +1,29 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+ alias(libs.plugins.kotlinx.serialization)
+}
+
+tasks.withType {
+ useJUnitPlatform()
+}
+
+dependencies {
+ implementation(projects.features.timers.domain)
+ implementation(projects.foundation.exposedUtils)
+ implementation(projects.foundation.hashing)
+ implementation(projects.foundation.pageToken)
+ implementation(projects.foundation.stateMachine)
+
+ implementation(libs.kotlinx.serialization.json)
+ implementation(libs.exposed.core)
+ implementation(libs.cache4k)
+
+ testImplementation(libs.mockk)
+ testImplementation(libs.kotlin.test)
+ testImplementation(libs.kotlinx.coroutines.test)
+
+ testImplementation(libs.exposed.jdbc)
+ testImplementation(libs.h2.database)
+
+ testImplementation(projects.foundation.validation.testsIntegration)
+}
\ No newline at end of file
diff --git a/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/CoPostgresqlTimerSessionRepository.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/CoPostgresqlTimerSessionRepository.kt
new file mode 100644
index 00000000..edfb1d0a
--- /dev/null
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/CoPostgresqlTimerSessionRepository.kt
@@ -0,0 +1,90 @@
+package org.timemates.backend.timers.data
+
+import com.timemates.backend.time.UnixTime
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.pagination.map
+import org.timemates.backend.timers.data.db.TableTimersSessionUsersDataSource
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+
+@OptIn(ValidationDelicateApi::class)
+class PostgresqlTimerSessionRepository(
+ coroutineScope: CoroutineScope =
+ CoroutineScope(Dispatchers.Default + SupervisorJob()),
+ private val tableTimersSessionUsers: TableTimersSessionUsersDataSource,
+) : TimerSessionRepository {
+
+ override suspend fun addUser(timerId: TimerId, userId: UserId, joinTime: UnixTime) {
+ tableTimersSessionUsers.assignUser(
+ timerId.long,
+ userId.long,
+ true,
+ joinTime.inMilliseconds,
+ )
+ }
+
+ override suspend fun removeUser(timerId: TimerId, userId: UserId) {
+ tableTimersSessionUsers.unassignUser(timerId.long, userId.long)
+ }
+
+ override suspend fun getTimerIdOfCurrentSession(userId: UserId, lastActiveTime: UnixTime): TimerId? {
+ return tableTimersSessionUsers.getTimerIdFromUserSession(userId.long, lastActiveTime.inMilliseconds)
+ ?.let { TimerId.createUnsafe(it) }
+ }
+
+ override suspend fun getMembers(
+ timerId: TimerId,
+ pageToken: PageToken?,
+ lastActiveTime: UnixTime,
+ ): Page {
+ return tableTimersSessionUsers.getUsers(
+ timerId.long,
+ pageToken,
+ lastActiveTime.inMilliseconds,
+ ).map { sessionUser -> UserId.createUnsafe(sessionUser.userId) }
+ }
+
+ override suspend fun getMembersCount(timerId: TimerId, activeAfterTime: UnixTime): Count {
+ return tableTimersSessionUsers.getUsersCount(timerId.long, activeAfterTime.inMilliseconds)
+ .let { Count.createUnsafe(it) }
+ }
+
+ override suspend fun setActiveUsersConfirmationRequirement(timerId: TimerId) {
+ tableTimersSessionUsers.setAllAsNotConfirmed(timerId.long)
+ }
+
+ override suspend fun markConfirmed(
+ timerId: TimerId,
+ userId: UserId,
+ confirmationTime: UnixTime,
+ ): Boolean {
+ tableTimersSessionUsers.assignUser(
+ timerId.long,
+ userId.long,
+ true,
+ confirmationTime.inMilliseconds
+ )
+
+ return true
+ }
+
+ override suspend fun removeInactiveUsers(afterTime: UnixTime) {
+ tableTimersSessionUsers.removeUsersBefore(afterTime.inMilliseconds)
+ }
+
+ override suspend fun removeNotConfirmedUsers(timerId: TimerId) {
+ tableTimersSessionUsers.removeNotConfirmedUsers(timerId.long)
+ }
+
+ override suspend fun updateLastActivityTime(timerId: TimerId, userId: UserId, time: UnixTime) {
+ tableTimersSessionUsers.updateLastActivityTime(timerId.long, userId.long, timerId.long)
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimerInvitesRepository.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/PostgresqlTimerInvitesRepository.kt
similarity index 60%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimerInvitesRepository.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/PostgresqlTimerInvitesRepository.kt
index 3eab6176..1ce7a18b 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimerInvitesRepository.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/PostgresqlTimerInvitesRepository.kt
@@ -1,19 +1,20 @@
-package io.timemates.backend.data.timers
+package org.timemates.backend.timers.data
import com.timemates.backend.time.UnixTime
-import io.timemates.backend.validation.createOrThrowInternally
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.data.timers.db.TableTimerInvitesDataSource
-import io.timemates.backend.data.timers.db.TableTimerParticipantsDataSource
-import io.timemates.backend.data.timers.mappers.TimerInvitesMapper
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.pagination.map
-import io.timemates.backend.timers.repositories.TimerInvitesRepository
-import io.timemates.backend.timers.types.Invite
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.UserId
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.pagination.map
+import org.timemates.backend.timers.data.db.TableTimerInvitesDataSource
+import org.timemates.backend.timers.data.db.TableTimerParticipantsDataSource
+import org.timemates.backend.timers.data.mappers.TimerInvitesMapper
+import org.timemates.backend.timers.domain.repositories.TimerInvitesRepository
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.Invite
+import org.timemates.backend.types.timers.value.InviteCode
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
class PostgresqlTimerInvitesRepository(
private val tableTimerInvitesDataSource: TableTimerInvitesDataSource,
@@ -34,9 +35,10 @@ class PostgresqlTimerInvitesRepository(
?.let(invitesMapper::dbInviteToDomainInvite)
}
+ @OptIn(ValidationDelicateApi::class)
override suspend fun getInvitesCount(timerId: TimerId, after: UnixTime): Count {
return participantsDataSource.getParticipantsCount(timerId.long, after.inMilliseconds)
- .let { Count.createOrThrowInternally(it.toInt()) }
+ .let { Count.createUnsafe(it.toInt()) }
}
override suspend fun createInvite(
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimersRepository.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/PostgresqlTimersRepository.kt
similarity index 76%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimersRepository.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/PostgresqlTimersRepository.kt
index a468b676..27b348ce 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/PostgresqlTimersRepository.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/PostgresqlTimersRepository.kt
@@ -1,23 +1,25 @@
-package io.timemates.backend.data.timers
+package org.timemates.backend.timers.data
import com.timemates.backend.time.UnixTime
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.data.timers.cache.CacheTimersDataSource
-import io.timemates.backend.data.timers.db.TableTimerParticipantsDataSource
-import io.timemates.backend.data.timers.db.TableTimersDataSource
-import io.timemates.backend.data.timers.mappers.TimersMapper
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.pagination.map
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerSettings
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.timers.types.value.TimerDescription
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.timers.types.value.TimerName
-import io.timemates.backend.users.types.value.UserId
-import io.timemates.backend.validation.createOrThrowInternally
-
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.pagination.map
+import org.timemates.backend.timers.data.cache.CacheTimersDataSource
+import org.timemates.backend.timers.data.db.TableTimerParticipantsDataSource
+import org.timemates.backend.timers.data.db.TableTimersDataSource
+import org.timemates.backend.timers.data.mappers.TimersMapper
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.TimerSettings
+import org.timemates.backend.types.timers.value.InviteCode
+import org.timemates.backend.types.timers.value.TimerDescription
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.timers.value.TimerName
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+
+@OptIn(ValidationDelicateApi::class)
class PostgresqlTimersRepository(
private val tableTimers: TableTimersDataSource,
private val cachedTimers: CacheTimersDataSource,
@@ -43,7 +45,7 @@ class PostgresqlTimersRepository(
settings = settings.let(timersMapper::domainSettingsToDbSettingsPatchable)
)
- return TimerId.createOrThrowInternally(id)
+ return TimerId.createUnsafe(id)
}
override suspend fun getTimerInformation(timerId: TimerId): TimersRepository.TimerInformation? {
@@ -93,12 +95,12 @@ class PostgresqlTimersRepository(
override suspend fun getMembers(timerId: TimerId, pageToken: PageToken?): Page {
return tableTimerParticipants.getParticipants(
timerId.long, pageToken,
- ).map { id -> UserId.createOrThrowInternally(id) }
+ ).map { id -> UserId.createUnsafe(id) }
}
override suspend fun getMembersCountOfInvite(timerId: TimerId, inviteCode: InviteCode): Count {
return tableTimerParticipants.getParticipantsCountOfInvite(timerId.long, inviteCode.string)
- .let { Count.createOrThrowInternally(it) }
+ .let { Count.createUnsafe(it) }
}
override suspend fun isMemberOf(userId: UserId, timerId: TimerId): Boolean {
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/cache/CacheTimersDataSource.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/CacheTimersDataSource.kt
similarity index 83%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/cache/CacheTimersDataSource.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/CacheTimersDataSource.kt
index f05f2d67..87a0e7cf 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/cache/CacheTimersDataSource.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/CacheTimersDataSource.kt
@@ -1,7 +1,7 @@
-package io.timemates.backend.data.timers.cache
+package org.timemates.backend.timers.data.cache
import io.github.reactivecircus.cache4k.Cache
-import io.timemates.backend.data.timers.cache.entities.CachedTimer
+import org.timemates.backend.timers.data.cache.entities.CachedTimer
class CacheTimersDataSource(maxCachedEntities: Long) {
private val cache = Cache.Builder()
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/cache/entities/CacheSessionUser.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/entities/CacheSessionUser.kt
similarity index 51%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/cache/entities/CacheSessionUser.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/entities/CacheSessionUser.kt
index 7111acbd..76b1808e 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/cache/entities/CacheSessionUser.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/entities/CacheSessionUser.kt
@@ -1,7 +1,6 @@
-package io.timemates.backend.data.timers.cache.entities
+package org.timemates.backend.timers.data.cache.entities
data class CacheSessionUser(
- val timerId: Long,
val userId: Long,
val lastActivityTime: Long,
)
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/cache/entities/CachedTimer.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/entities/CachedTimer.kt
similarity index 87%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/cache/entities/CachedTimer.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/entities/CachedTimer.kt
index 809ac6f7..3cee50f0 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/cache/entities/CachedTimer.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/cache/entities/CachedTimer.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.timers.cache.entities
+package org.timemates.backend.timers.data.cache.entities
data class CachedTimer(
val name: String,
diff --git a/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/PostgresqlStateStorageRepository.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/PostgresqlStateStorageRepository.kt
new file mode 100644
index 00000000..f13456fd
--- /dev/null
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/PostgresqlStateStorageRepository.kt
@@ -0,0 +1,29 @@
+package org.timemates.backend.timers.data.db
+
+import org.timemates.backend.fsm.StateStorage
+import org.timemates.backend.timers.data.mappers.TimerSessionMapper
+import org.timemates.backend.types.timers.TimerEvent
+import org.timemates.backend.types.timers.TimerState
+import org.timemates.backend.types.timers.value.TimerId
+
+class PostgresqlStateStorageRepository(
+ private val tableTimersStateDataSource: TableTimersStateDataSource,
+ private val sessionsMapper: TimerSessionMapper,
+) : StateStorage {
+ override suspend fun save(key: TimerId, state: TimerState) {
+ tableTimersStateDataSource.setTimerState(key, sessionsMapper.fsmStateToDbState(state))
+ }
+
+ override suspend fun remove(key: TimerId): Boolean {
+ return tableTimersStateDataSource.removeTimerState(key.long)
+ }
+
+ override suspend fun load(key: TimerId): TimerState? {
+ return tableTimersStateDataSource.getTimerState(key.long)
+ ?.let {
+ sessionsMapper.dbStateToFsmState(
+ dbState = it,
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimerInvitesDataSource.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimerInvitesDataSource.kt
similarity index 79%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimerInvitesDataSource.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimerInvitesDataSource.kt
index d9c741a1..f5a05660 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimerInvitesDataSource.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimerInvitesDataSource.kt
@@ -1,24 +1,24 @@
-package io.timemates.backend.data.timers.db
+package org.timemates.backend.timers.data.db
-import io.timemates.backend.data.timers.db.entities.DbInvite
-import io.timemates.backend.data.timers.db.entities.InvitesPageToken
-import io.timemates.backend.data.timers.db.tables.TimersInvitesTable
-import io.timemates.backend.data.timers.mappers.TimerInvitesMapper
-import io.timemates.backend.exposed.suspendedTransaction
-import io.timemates.backend.pagination.Ordering
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
+import org.timemates.backend.exposed.suspendedTransaction
+import org.timemates.backend.pagination.Ordering
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.timers.data.db.entities.DbInvite
+import org.timemates.backend.timers.data.db.entities.InvitesPageToken
+import org.timemates.backend.timers.data.db.tables.TimersInvitesTable
+import org.timemates.backend.timers.data.mappers.TimerInvitesMapper
class TableTimerInvitesDataSource(
private val database: Database,
private val invitesMapper: TimerInvitesMapper,
- private val json: Json,
+ private val json: Json = Json,
) {
init {
transaction(database) {
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimerParticipantsDataSource.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimerParticipantsDataSource.kt
similarity index 88%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimerParticipantsDataSource.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimerParticipantsDataSource.kt
index 6d16d6cb..2d495c5c 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimerParticipantsDataSource.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimerParticipantsDataSource.kt
@@ -1,11 +1,5 @@
-package io.timemates.backend.data.timers.db
+package org.timemates.backend.timers.data.db
-import io.timemates.backend.data.timers.db.entities.TimerParticipantPageToken
-import io.timemates.backend.data.timers.db.tables.TimersParticipantsTable
-import io.timemates.backend.exposed.suspendedTransaction
-import io.timemates.backend.pagination.Ordering
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -13,10 +7,16 @@ import org.jetbrains.annotations.TestOnly
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
+import org.timemates.backend.exposed.suspendedTransaction
+import org.timemates.backend.pagination.Ordering
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.timers.data.db.entities.TimerParticipantPageToken
+import org.timemates.backend.timers.data.db.tables.TimersParticipantsTable
class TableTimerParticipantsDataSource(
private val database: Database,
- private val json: Json,
+ private val json: Json = Json,
) {
init {
transaction(database) {
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersDataSource.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersDataSource.kt
similarity index 86%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersDataSource.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersDataSource.kt
index f2699fd1..75db473b 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersDataSource.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersDataSource.kt
@@ -1,25 +1,26 @@
-package io.timemates.backend.data.timers.db
+package org.timemates.backend.timers.data.db
-import io.timemates.backend.data.timers.db.entities.DbTimer
-import io.timemates.backend.data.timers.db.entities.TimersPageToken
-import io.timemates.backend.data.timers.db.tables.TimersTable
-import io.timemates.backend.data.timers.mappers.TimersMapper
-import io.timemates.backend.exposed.suspendedTransaction
-import io.timemates.backend.exposed.update
-import io.timemates.backend.pagination.Ordering
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
+import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import org.jetbrains.annotations.TestOnly
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
+import org.timemates.backend.exposed.suspendedTransaction
+import org.timemates.backend.exposed.update
+import org.timemates.backend.pagination.Ordering
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.timers.data.db.entities.DbTimer
+import org.timemates.backend.timers.data.db.entities.TimersPageToken
+import org.timemates.backend.timers.data.db.tables.TimersTable
+import org.timemates.backend.timers.data.mappers.TimersMapper
class TableTimersDataSource(
private val database: Database,
private val timersMapper: TimersMapper,
- private val json: Json,
+ private val json: Json = Json,
) {
init {
transaction {
@@ -104,7 +105,7 @@ class TableTimersDataSource(
userId: Long,
pageToken: PageToken?,
): Page = suspendedTransaction(database) {
- val decodedPageToken: TimersPageToken? = pageToken?.forInternal()?.let { json.decodeFromString(it) }
+ val decodedPageToken: TimersPageToken? = pageToken?.forInternal()?.let(json::decodeFromString)
val result = TimersTable.select {
TimersTable.CREATION_TIME less (decodedPageToken?.beforeTime ?: Long.MAX_VALUE) and
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersSessionUsersDataSource.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersSessionUsersDataSource.kt
similarity index 88%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersSessionUsersDataSource.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersSessionUsersDataSource.kt
index 8ba9c42c..d6a14fc6 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersSessionUsersDataSource.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersSessionUsersDataSource.kt
@@ -1,15 +1,5 @@
-package io.timemates.backend.data.timers.db
-
-import io.timemates.backend.data.timers.db.entities.DbSessionUser
-import io.timemates.backend.data.timers.db.entities.TimerParticipantPageToken
-import io.timemates.backend.data.timers.db.tables.TimersSessionUsersTable
-import io.timemates.backend.data.timers.mappers.TimerSessionMapper
-import io.timemates.backend.exposed.suspendedTransaction
-import io.timemates.backend.exposed.update
-import io.timemates.backend.exposed.upsert
-import io.timemates.backend.pagination.Ordering
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
+package org.timemates.backend.timers.data.db
+
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
@@ -17,11 +7,21 @@ import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.SqlExpressionBuilder.lessEq
import org.jetbrains.exposed.sql.transactions.transaction
+import org.timemates.backend.exposed.suspendedTransaction
+import org.timemates.backend.exposed.update
+import org.timemates.backend.exposed.upsert
+import org.timemates.backend.pagination.Ordering
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.timers.data.db.entities.DbSessionUser
+import org.timemates.backend.timers.data.db.entities.TimerParticipantPageToken
+import org.timemates.backend.timers.data.db.tables.TimersSessionUsersTable
+import org.timemates.backend.timers.data.mappers.TimerSessionMapper
class TableTimersSessionUsersDataSource(
private val database: Database,
private val timerSessionMapper: TimerSessionMapper,
- private val json: Json,
+ private val json: Json = Json,
) {
init {
transaction(database) {
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersStateDataSource.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersStateDataSource.kt
similarity index 78%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersStateDataSource.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersStateDataSource.kt
index d31d5a80..07b6972e 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/TableTimersStateDataSource.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/TableTimersStateDataSource.kt
@@ -1,14 +1,15 @@
-package io.timemates.backend.data.timers.db
+package org.timemates.backend.timers.data.db
-import io.timemates.backend.data.timers.db.entities.DbTimer
-import io.timemates.backend.data.timers.db.tables.TimersStateTable
-import io.timemates.backend.data.timers.mappers.TimersMapper
-import io.timemates.backend.exposed.suspendedTransaction
-import io.timemates.backend.exposed.update
import org.jetbrains.annotations.TestOnly
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.SqlExpressionBuilder.eq
import org.jetbrains.exposed.sql.transactions.transaction
+import org.timemates.backend.exposed.suspendedTransaction
+import org.timemates.backend.exposed.update
+import org.timemates.backend.timers.data.db.entities.DbTimer
+import org.timemates.backend.timers.data.db.tables.TimersStateTable
+import org.timemates.backend.timers.data.mappers.TimersMapper
+import org.timemates.backend.types.timers.value.TimerId
class TableTimersStateDataSource(
private val database: Database,
@@ -26,8 +27,8 @@ class TableTimersStateDataSource(
?.let(timersMapper::resultRowToTimerState)
}
- suspend fun setTimerState(state: DbTimer.State) = suspendedTransaction(database) {
- val condition = Op.build { TimersStateTable.TIMER_ID eq state.timerId }
+ suspend fun setTimerState(timerId: TimerId, state: DbTimer.State) = suspendedTransaction(database) {
+ val condition = Op.build { TimersStateTable.TIMER_ID eq timerId.long }
val exists = TimersStateTable.select(condition).empty()
if (exists) {
@@ -37,7 +38,7 @@ class TableTimersStateDataSource(
}
} else {
TimersStateTable.insert {
- it[TIMER_ID] = state.timerId
+ it[TIMER_ID] = timerId.long
it[PHASE] = state.phase
it[ENDS_AT] = state.endsAt
}
@@ -69,7 +70,7 @@ class TableTimersStateDataSource(
TimersStateTable.select { TimersStateTable.TIMER_ID inList ids }
.associate {
val state = timersMapper.resultRowToTimerState(it)
- state.timerId to state
+ it[TimersStateTable.TIMER_ID] to state
}
}
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbInvite.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbInvite.kt
similarity index 69%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbInvite.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbInvite.kt
index 7ba148a3..50bf9180 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbInvite.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbInvite.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.timers.db.entities
+package org.timemates.backend.timers.data.db.entities
class DbInvite(
val timerId: Long,
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbSessionUser.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbSessionUser.kt
similarity index 66%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbSessionUser.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbSessionUser.kt
index 7840428b..cbf29fdb 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbSessionUser.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbSessionUser.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.timers.db.entities
+package org.timemates.backend.timers.data.db.entities
data class DbSessionUser(
val timerId: Long,
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbTimer.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbTimer.kt
similarity index 92%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbTimer.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbTimer.kt
index 6bee27bb..0495cb0d 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/DbTimer.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/DbTimer.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.timers.db.entities
+package org.timemates.backend.timers.data.db.entities
data class DbTimer(
val id: Long,
@@ -29,7 +29,6 @@ data class DbTimer(
}
class State(
- val timerId: Long,
val phase: Phase,
val endsAt: Long?,
val creationTime: Long,
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/InvitesPageToken.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/InvitesPageToken.kt
similarity index 65%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/InvitesPageToken.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/InvitesPageToken.kt
index d64b736a..b4f076d8 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/InvitesPageToken.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/InvitesPageToken.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.timers.db.entities
+package org.timemates.backend.timers.data.db.entities
import kotlinx.serialization.Serializable
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimerParticipantPageToken.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/TimerParticipantPageToken.kt
similarity index 69%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimerParticipantPageToken.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/TimerParticipantPageToken.kt
index 3d0a1ae4..63f9a9bb 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimerParticipantPageToken.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/TimerParticipantPageToken.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.timers.db.entities
+package org.timemates.backend.timers.data.db.entities
import kotlinx.serialization.Serializable
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimersPageToken.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/TimersPageToken.kt
similarity index 72%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimersPageToken.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/TimersPageToken.kt
index 243c69a4..8dbdfcd9 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/entities/TimersPageToken.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/entities/TimersPageToken.kt
@@ -1,4 +1,4 @@
-package io.timemates.backend.data.timers.db.entities
+package org.timemates.backend.timers.data.db.entities
import kotlinx.serialization.Serializable
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersInvitesTable.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersInvitesTable.kt
similarity index 80%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersInvitesTable.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersInvitesTable.kt
index 0242c39f..b1654e90 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersInvitesTable.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersInvitesTable.kt
@@ -1,6 +1,5 @@
-package io.timemates.backend.data.timers.db.tables
+package org.timemates.backend.timers.data.db.tables
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource
import org.jetbrains.exposed.sql.Table
internal object TimersInvitesTable : Table("timers_invites") {
@@ -18,7 +17,6 @@ internal object TimersInvitesTable : Table("timers_invites") {
val MAX_JOINERS_COUNT = integer("max_joiners_count")
val CREATOR_ID = long("creator_id")
- .references(PostgresqlUsersDataSource.UsersTable.USER_ID)
/**
* Denotes whether invite code active or not.
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersParticipantsTable.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersParticipantsTable.kt
similarity index 75%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersParticipantsTable.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersParticipantsTable.kt
index d9571334..497fd919 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersParticipantsTable.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersParticipantsTable.kt
@@ -1,11 +1,10 @@
-package io.timemates.backend.data.timers.db.tables
+package org.timemates.backend.timers.data.db.tables
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource.UsersTable
import org.jetbrains.exposed.sql.Table
internal object TimersParticipantsTable : Table("timers_participants") {
val TIMER_ID = long("timer_id").references(TimersTable.ID)
- val USER_ID = long("user_id").references(UsersTable.USER_ID)
+ val USER_ID = long("user_id")
/**
* Denotes the invitation code that was used to join the timer.
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersSessionUsersTable.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersSessionUsersTable.kt
similarity index 79%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersSessionUsersTable.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersSessionUsersTable.kt
index ab8bf735..3b541cc1 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersSessionUsersTable.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersSessionUsersTable.kt
@@ -1,6 +1,5 @@
-package io.timemates.backend.data.timers.db.tables
+package org.timemates.backend.timers.data.db.tables
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource
import org.jetbrains.exposed.sql.Table
/**
@@ -12,7 +11,6 @@ import org.jetbrains.exposed.sql.Table
internal object TimersSessionUsersTable : Table("timers_sessions") {
val TIMER_ID = long("timer_id").references(TimersTable.ID)
val USER_ID = long("user_id")
- .references(PostgresqlUsersDataSource.UsersTable.USER_ID)
/**
* Denotes that the user has confirmed their attendance.
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersStateTable.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersStateTable.kt
similarity index 60%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersStateTable.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersStateTable.kt
index 1344ad26..8d4c999a 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersStateTable.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersStateTable.kt
@@ -1,12 +1,12 @@
-package io.timemates.backend.data.timers.db.tables
+package org.timemates.backend.timers.data.db.tables
-import io.timemates.backend.data.timers.db.entities.DbTimer.State.Phase
import org.jetbrains.exposed.sql.Table
+import org.timemates.backend.timers.data.db.entities.DbTimer
internal object TimersStateTable : Table("timers_state") {
val TIMER_ID = long("timer_id")
.references(TimersTable.ID)
- val PHASE = enumeration("phase")
+ val PHASE = enumeration("phase")
val ENDS_AT = long("ends_at").nullable()
val CREATION_TIME = long("creation_time")
}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersTable.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersTable.kt
similarity index 79%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersTable.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersTable.kt
index 82871359..4676cc6d 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/db/tables/TimersTable.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/db/tables/TimersTable.kt
@@ -1,8 +1,7 @@
-package io.timemates.backend.data.timers.db.tables
+package org.timemates.backend.timers.data.db.tables
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource
-import io.timemates.backend.exposed.emptyAsDefault
import org.jetbrains.exposed.sql.Table
+import org.timemates.backend.exposed.emptyAsDefault
import kotlin.time.Duration.Companion.minutes
internal object TimersTable : Table("timers") {
@@ -10,7 +9,6 @@ internal object TimersTable : Table("timers") {
val NAME = varchar("name", 50)
val DESCRIPTION = varchar("description", 200).emptyAsDefault()
val OWNER_ID = long("owner_id")
- .references(PostgresqlUsersDataSource.UsersTable.USER_ID)
val CREATION_TIME = long("creation_time")
// Settings
diff --git a/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimerInvitesMapper.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimerInvitesMapper.kt
new file mode 100644
index 00000000..d240fc6c
--- /dev/null
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimerInvitesMapper.kt
@@ -0,0 +1,33 @@
+package org.timemates.backend.timers.data.mappers
+
+import com.timemates.backend.time.UnixTime
+import org.jetbrains.exposed.sql.ResultRow
+import org.timemates.backend.timers.data.db.entities.DbInvite
+import org.timemates.backend.timers.data.db.tables.TimersInvitesTable
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.Invite
+import org.timemates.backend.types.timers.value.InviteCode
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+
+@OptIn(ValidationDelicateApi::class)
+class TimerInvitesMapper {
+ fun resultRowToDbInvite(resultRow: ResultRow): DbInvite = with(resultRow) {
+ return DbInvite(
+ timerId = get(TimersInvitesTable.TIMER_ID),
+ maxJoiners = get(TimersInvitesTable.MAX_JOINERS_COUNT),
+ inviteCode = get(TimersInvitesTable.INVITE_CODE),
+ creationTime = get(TimersInvitesTable.CREATION_TIME),
+ )
+ }
+
+ fun dbInviteToDomainInvite(dbInvite: DbInvite): Invite = with(dbInvite) {
+ return Invite(
+ timerId = TimerId.createUnsafe(timerId),
+ code = InviteCode.createUnsafe(inviteCode),
+ creationTime = UnixTime.createUnsafe(creationTime),
+ limit = Count.createUnsafe(maxJoiners),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimerSessionMapper.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimerSessionMapper.kt
new file mode 100644
index 00000000..03917387
--- /dev/null
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimerSessionMapper.kt
@@ -0,0 +1,76 @@
+package org.timemates.backend.timers.data.mappers
+
+import com.timemates.backend.time.UnixTime
+import org.jetbrains.exposed.sql.ResultRow
+import org.timemates.backend.timers.data.db.entities.DbSessionUser
+import org.timemates.backend.timers.data.db.entities.DbTimer
+import org.timemates.backend.timers.data.db.tables.TimersSessionUsersTable
+import org.timemates.backend.types.timers.TimerState
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
+import kotlin.time.Duration.Companion.milliseconds
+
+@OptIn(ValidationDelicateApi::class)
+class TimerSessionMapper {
+ fun resultRowToSessionUser(resultRow: ResultRow): DbSessionUser = with(resultRow) {
+ return DbSessionUser(
+ timerId = get(TimersSessionUsersTable.TIMER_ID),
+ userId = get(TimersSessionUsersTable.USER_ID),
+ lastActivityTime = get(TimersSessionUsersTable.LAST_ACTIVITY_TIME),
+ )
+ }
+
+ fun dbStateToFsmState(
+ dbState: DbTimer.State,
+ ): TimerState = with(dbState) {
+ val publishTime = UnixTime.createUnsafe(creationTime)
+ val alive = (endsAt?.let { (creationTime - it) } ?: Long.MAX_VALUE).milliseconds
+
+ val state = when (phase) {
+ DbTimer.State.Phase.ATTENDANCE_CONFIRMATION -> TimerState.ConfirmationWaiting(
+ publishTime = publishTime,
+ alive = alive,
+ )
+
+ DbTimer.State.Phase.OFFLINE -> TimerState.Inactive(
+ publishTime = publishTime,
+ )
+
+ DbTimer.State.Phase.PAUSED ->
+ TimerState.Paused(
+ publishTime = UnixTime.createUnsafe(creationTime),
+ )
+
+ DbTimer.State.Phase.REST ->
+ TimerState.Rest(
+ alive = alive,
+ publishTime = publishTime,
+ )
+
+ DbTimer.State.Phase.RUNNING ->
+ TimerState.Running(
+ alive = alive,
+ publishTime = publishTime,
+ )
+ }
+
+ return@with state
+ }
+
+
+ fun fsmStateToDbState(state: TimerState): DbTimer.State = with(state) {
+ val phase = when (state) {
+ is TimerState.ConfirmationWaiting -> DbTimer.State.Phase.ATTENDANCE_CONFIRMATION
+ is TimerState.Inactive -> DbTimer.State.Phase.OFFLINE
+ is TimerState.Paused -> DbTimer.State.Phase.PAUSED
+ is TimerState.Rest -> DbTimer.State.Phase.REST
+ is TimerState.Running -> DbTimer.State.Phase.RUNNING
+ }
+
+ return@with DbTimer.State(
+ phase = phase,
+ endsAt = (publishTime + alive).inMilliseconds,
+ creationTime = publishTime.inMilliseconds
+ )
+ }
+}
\ No newline at end of file
diff --git a/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimersMapper.kt b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimersMapper.kt
similarity index 64%
rename from data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimersMapper.kt
rename to features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimersMapper.kt
index 2e48f526..e8c0510e 100644
--- a/data/src/main/kotlin/io/timemates/backend/data/timers/mappers/TimersMapper.kt
+++ b/features/timers/data/src/main/kotlin/org/timemates/backend/timers/data/mappers/TimersMapper.kt
@@ -1,26 +1,25 @@
@file:Suppress("MemberVisibilityCanBePrivate")
-package io.timemates.backend.data.timers.mappers
+package org.timemates.backend.timers.data.mappers
-import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.data.common.markers.Mapper
-import io.timemates.backend.data.timers.db.entities.DbTimer
-import io.timemates.backend.data.timers.db.tables.TimersStateTable
-import io.timemates.backend.data.timers.db.tables.TimersTable
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.Timer
-import io.timemates.backend.timers.types.TimerSettings
-import io.timemates.backend.timers.types.value.TimerDescription
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.timers.types.value.TimerName
-import io.timemates.backend.users.types.value.UserId
-import io.timemates.backend.validation.createOrThrowInternally
import org.jetbrains.exposed.sql.ResultRow
+import org.timemates.backend.timers.data.db.entities.DbTimer
+import org.timemates.backend.timers.data.db.tables.TimersStateTable
+import org.timemates.backend.timers.data.db.tables.TimersTable
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.Timer
+import org.timemates.backend.types.timers.TimerSettings
+import org.timemates.backend.types.timers.value.TimerDescription
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.timers.value.TimerName
+import org.timemates.backend.types.users.value.UserId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
import kotlin.time.Duration.Companion.minutes
-class TimersMapper(private val sessionMapper: TimerSessionMapper) : Mapper {
+@OptIn(ValidationDelicateApi::class)
+class TimersMapper(private val sessionMapper: TimerSessionMapper) {
fun resultRowToDbTimer(resultRow: ResultRow) = with(resultRow) {
return@with DbTimer(
get(TimersTable.ID),
@@ -46,7 +45,6 @@ class TimersMapper(private val sessionMapper: TimerSessionMapper) : Mapper {
fun resultRowToTimerState(resultRow: ResultRow) = with(resultRow) {
return@with DbTimer.State(
- get(TimersStateTable.TIMER_ID),
get(TimersStateTable.PHASE),
get(TimersStateTable.ENDS_AT),
get(TimersStateTable.CREATION_TIME),
@@ -57,23 +55,17 @@ class TimersMapper(private val sessionMapper: TimerSessionMapper) : Mapper {
dbTimer: DbTimer,
membersCount: Int,
state: DbTimer.State,
- timeProvider: TimeProvider,
- timersRepository: TimersRepository,
- timerSessionRepository: TimerSessionRepository,
): Timer {
return Timer(
- id = TimerId.createOrThrowInternally(dbTimer.id),
- name = TimerName.createOrThrowInternally(dbTimer.name),
+ id = TimerId.createUnsafe(dbTimer.id),
+ name = TimerName.createUnsafe(dbTimer.name),
description = dbTimer.description.takeUnless { it.isBlank() }
- ?.let { TimerDescription.createOrThrowInternally(it) },
- ownerId = UserId.createOrThrowInternally(dbTimer.ownerId),
+ ?.let { TimerDescription.createUnsafe(it) },
+ ownerId = UserId.createUnsafe(dbTimer.ownerId),
settings = dbSettingsToDomainSettings(dbTimer.settings),
- membersCount = Count.createOrThrowInternally(membersCount),
+ membersCount = Count.createUnsafe(membersCount),
state = sessionMapper.dbStateToFsmState(
dbState = state,
- timeProvider = timeProvider,
- timersRepository = timersRepository,
- timersSessionRepository = timerSessionRepository
),
)
}
@@ -86,7 +78,7 @@ class TimersMapper(private val sessionMapper: TimerSessionMapper) : Mapper {
restTime = restTime.minutes,
bigRestTime = bigRestTime.minutes,
bigRestEnabled = bigRestEnabled,
- bigRestPer = Count.createOrThrowInternally(bigRestPer),
+ bigRestPer = Count.createUnsafe(bigRestPer),
isEveryoneCanPause = isEveryoneCanPause,
isConfirmationRequired = isConfirmationRequired,
)
@@ -121,12 +113,12 @@ class TimersMapper(private val sessionMapper: TimerSessionMapper) : Mapper {
membersCount: Int,
): TimersRepository.TimerInformation = with(dbTimer) {
return@with TimersRepository.TimerInformation(
- id = TimerId.createOrThrowInternally(id),
- name = TimerName.createOrThrowInternally(name),
- description = TimerDescription.createOrThrowInternally(description),
- ownerId = UserId.createOrThrowInternally(ownerId),
+ id = TimerId.createUnsafe(id),
+ name = TimerName.createUnsafe(name),
+ description = TimerDescription.createUnsafe(description),
+ ownerId = UserId.createUnsafe(ownerId),
settings = dbSettingsToDomainSettings(settings),
- membersCount = Count.createOrThrowInternally(membersCount),
+ membersCount = Count.createUnsafe(membersCount),
)
}
}
diff --git a/data/src/test/kotlin/io/timemates/backend/data/timers/datasource/TableTimersDataSourceTest.kt b/features/timers/data/src/test/kotlin/org/timemates/backend/timers/data/test/datasource/TableTimersDataSourceTest.kt
similarity index 76%
rename from data/src/test/kotlin/io/timemates/backend/data/timers/datasource/TableTimersDataSourceTest.kt
rename to features/timers/data/src/test/kotlin/org/timemates/backend/timers/data/test/datasource/TableTimersDataSourceTest.kt
index 58c4307c..660c5e5f 100644
--- a/data/src/test/kotlin/io/timemates/backend/data/timers/datasource/TableTimersDataSourceTest.kt
+++ b/features/timers/data/src/test/kotlin/org/timemates/backend/timers/data/test/datasource/TableTimersDataSourceTest.kt
@@ -1,18 +1,14 @@
-package io.timemates.backend.data.timers.datasource
-
-import io.timemates.backend.data.timers.db.TableTimersDataSource
-import io.timemates.backend.data.timers.db.entities.DbTimer
-import io.timemates.backend.data.timers.mappers.TimerSessionMapper
-import io.timemates.backend.data.timers.mappers.TimersMapper
-import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource
-import kotlinx.coroutines.runBlocking
+package org.timemates.backend.timers.data.test.datasource
+
+import kotlinx.coroutines.test.runTest
import kotlinx.serialization.json.Json
import org.jetbrains.exposed.sql.Database
-import org.junit.After
-import org.junit.Before
-import org.junit.Test
import org.junit.jupiter.api.BeforeEach
-import kotlin.properties.Delegates
+import org.timemates.backend.timers.data.db.TableTimersDataSource
+import org.timemates.backend.timers.data.db.entities.DbTimer
+import org.timemates.backend.timers.data.mappers.TimerSessionMapper
+import org.timemates.backend.timers.data.mappers.TimersMapper
+import kotlin.test.Test
import kotlin.test.assertContentEquals
import kotlin.test.assertEquals
import kotlin.test.assertNull
@@ -23,28 +19,17 @@ class TableTimersDataSourceTest {
private val database = Database.connect(databaseUrl, databaseDriver)
private val timers: TableTimersDataSource = TableTimersDataSource(database, TimersMapper(TimerSessionMapper()), Json)
- private val users: PostgresqlUsersDataSource = PostgresqlUsersDataSource(database)
- private var ownerId by Delegates.notNull()
+ private var ownerId = 1L
+ private var anotherUser = 2L
- @After
@BeforeEach
- fun `clear database`(): Unit = runBlocking {
+ fun `clear database`(): Unit = runTest {
timers.clear()
}
- @Before
- fun `create test user`(): Unit = runBlocking {
- ownerId = users.createUser(
- email = "test@email.com",
- userName = "test name",
- shortBio = "Test bio",
- creationTime = System.currentTimeMillis(),
- )
- }
-
@Test
- fun `createTimer should return the correct timer ID`(): Unit = runBlocking {
+ fun `createTimer should return the correct timer ID`(): Unit = runTest {
// Arrange
val name = "Test Timer"
val description = "This is a test timer"
@@ -55,7 +40,7 @@ class TableTimersDataSourceTest {
}
@Test
- fun `createTimer should return a different timer ID for each call`(): Unit = runBlocking {
+ fun `createTimer should return a different timer ID for each call`(): Unit = runTest {
// Arrange
val name = "Test Timer"
val description = "This is a test timer"
@@ -70,7 +55,7 @@ class TableTimersDataSourceTest {
}
@Test
- fun `createTimer should return a default description if not provided`(): Unit = runBlocking {
+ fun `createTimer should return a default description if not provided`(): Unit = runTest {
// Arrange
val name = "Test Timer"
val creationTime = System.currentTimeMillis()
@@ -83,7 +68,7 @@ class TableTimersDataSourceTest {
}
@Test
- fun `editTimer updates name successfully`(): Unit = runBlocking {
+ fun `editTimer updates name successfully`(): Unit = runTest {
val name = "Test Timer"
val creationTime = System.currentTimeMillis()
@@ -96,7 +81,7 @@ class TableTimersDataSourceTest {
}
@Test
- fun `editTimer with timer that does not exist`(): Unit = runBlocking {
+ fun `editTimer with timer that does not exist`(): Unit = runTest {
val timerId = 999L
val newName = "New Timer Name"
timers.editTimer(timerId, newName = newName)
@@ -105,7 +90,7 @@ class TableTimersDataSourceTest {
}
@Test
- fun `setSettings should update the timer settings`(): Unit = runBlocking {
+ fun `setSettings should update the timer settings`(): Unit = runTest {
// Arrange
val name = "Test Timer"
val creationTime = System.currentTimeMillis()
@@ -124,12 +109,10 @@ class TableTimersDataSourceTest {
)
// Act
- runBlocking {
- timers.setSettings(timerId, settings)
- }
+ timers.setSettings(timerId, settings)
// Assert
- val updatedTimer = runBlocking { timers.getTimer(timerId) }
+ val updatedTimer = timers.getTimer(timerId)
assertEquals(settings.workTime, updatedTimer?.settings?.workTime)
assertEquals(settings.bigRestEnabled, updatedTimer?.settings?.bigRestEnabled)
assertEquals(settings.bigRestPer, updatedTimer?.settings?.bigRestPer)
@@ -141,17 +124,14 @@ class TableTimersDataSourceTest {
@Test
- fun `check get timers should return empty list if no timers`(): Unit = runBlocking {
- val anotherUser = users.createUser("test2@gmail.com", "Test2", null, System.currentTimeMillis())
+ fun `check get timers should return empty list if no timers`(): Unit = runTest {
timers.createTimer("Test", null, anotherUser, System.currentTimeMillis())
assert(timers.getTimers(ownerId, pageToken = null).value.isEmpty())
}
@Test
- fun `check get timers should return correct list of timers`(): Unit = runBlocking {
- val anotherUser = users.createUser("test2@gmail.com", "Test2", null, System.currentTimeMillis())
-
+ fun `check get timers should return correct list of timers`(): Unit = runTest {
val ids = buildList {
add(timers.createTimer("Test", null, anotherUser, System.currentTimeMillis()))
add(timers.createTimer("Test 2", null, anotherUser, System.currentTimeMillis()))
@@ -164,7 +144,7 @@ class TableTimersDataSourceTest {
}
@Test
- fun `check get timers with page token returns correct page`(): Unit = runBlocking {
+ fun `check get timers with page token returns correct page`(): Unit = runTest {
List(50) { index ->
timers.createTimer("Test ${index + 1}", null, ownerId, creationTime = index.toLong())
}
diff --git a/features/timers/dependencies/build.gradle.kts b/features/timers/dependencies/build.gradle.kts
new file mode 100644
index 00000000..c2f2fc04
--- /dev/null
+++ b/features/timers/dependencies/build.gradle.kts
@@ -0,0 +1,22 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+ alias(libs.plugins.ksp)
+}
+
+dependencies {
+ implementation(projects.features.timers.domain)
+ implementation(projects.features.timers.data)
+ implementation(projects.foundation.stateMachine)
+ implementation(projects.features.timers.adapters)
+ implementation(projects.features.users.domain)
+
+ implementation(projects.foundation.random)
+
+ implementation(libs.kotlinx.serialization.json)
+
+ implementation(libs.exposed.core)
+
+ implementation(libs.koin.core)
+ implementation(libs.koin.annotations)
+ ksp(libs.koin.ksp.compiler)
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/TimersModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/TimersModule.kt
new file mode 100644
index 00000000..482452b0
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/TimersModule.kt
@@ -0,0 +1,27 @@
+package org.timemates.backend.timers.deps
+
+import org.koin.core.annotation.Module
+import org.timemates.backend.timers.deps.fsm.TimerStateMachineModule
+import org.timemates.backend.timers.deps.repositories.AdaptersModule
+import org.timemates.backend.timers.deps.repositories.TimersRepositoriesModule
+import org.timemates.backend.timers.deps.repositories.TimersSessionsRepositoriesModule
+import org.timemates.backend.timers.deps.usecases.TimerInvitesUseCasesModule
+import org.timemates.backend.timers.deps.usecases.TimerMembersUseCasesModule
+import org.timemates.backend.timers.deps.usecases.TimerSessionsUseCaseModule
+import org.timemates.backend.timers.deps.usecases.TimersUseCasesModule
+
+@Module(
+ includes = [
+ TimersRepositoriesModule::class,
+ TimersSessionsRepositoriesModule::class,
+ TimerStateMachineModule::class,
+
+ TimerInvitesUseCasesModule::class,
+ TimerSessionsUseCaseModule::class,
+ TimersUseCasesModule::class,
+ TimerMembersUseCasesModule::class,
+
+ AdaptersModule::class,
+ ]
+)
+class TimersModule
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/fsm/TimerStateMachineModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/fsm/TimerStateMachineModule.kt
new file mode 100644
index 00000000..d65f4b54
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/fsm/TimerStateMachineModule.kt
@@ -0,0 +1,55 @@
+package org.timemates.backend.timers.deps.fsm
+
+import com.timemates.backend.time.TimeProvider
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.fsm.StateStorage
+import org.timemates.backend.timers.data.db.PostgresqlStateStorageRepository
+import org.timemates.backend.timers.data.db.TableTimersStateDataSource
+import org.timemates.backend.timers.data.mappers.TimerSessionMapper
+import org.timemates.backend.timers.deps.repositories.database.TimersTablesModule
+import org.timemates.backend.timers.deps.repositories.mappers.TimersMapperModule
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimerEvent
+import org.timemates.backend.types.timers.TimerState
+import org.timemates.backend.types.timers.value.TimerId
+
+@Module(
+ includes = [
+ TimersMapperModule::class,
+ TimersTablesModule::class,
+ ]
+)
+class TimerStateMachineModule {
+ @Factory
+ fun stateStorage(
+ timersStateDataSource: TableTimersStateDataSource,
+ sessionMapper: TimerSessionMapper,
+ ): StateStorage {
+ return PostgresqlStateStorageRepository(
+ tableTimersStateDataSource = timersStateDataSource,
+ sessionsMapper = sessionMapper,
+ )
+ }
+
+ @Factory
+ fun timersFSM(
+ timers: TimersRepository,
+ sessions: TimerSessionRepository,
+ storage: StateStorage,
+ timeProvider: TimeProvider,
+ ): TimersStateMachine {
+ return TimersStateMachine(
+ timers = timers,
+ sessions = sessions,
+ storage = storage,
+ timeProvider = timeProvider,
+ coroutineScope = CoroutineScope(Dispatchers.IO + SupervisorJob()),
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/AdaptersModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/AdaptersModule.kt
new file mode 100644
index 00000000..7234b681
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/AdaptersModule.kt
@@ -0,0 +1,15 @@
+package org.timemates.backend.timers.deps.repositories
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.auth.adapters.UsersRepositoryAdapter
+import org.timemates.backend.timers.domain.repositories.UsersRepository
+import org.timemates.backend.users.domain.repositories.UsersRepository as UsersRepositoryDelegate
+
+@Module
+class AdaptersModule {
+ @Factory
+ fun usersRepositoryAdapter(delegate: UsersRepositoryDelegate): UsersRepository {
+ return UsersRepositoryAdapter(delegate)
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/TimersRepositoriesModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/TimersRepositoriesModule.kt
new file mode 100644
index 00000000..eea0bba1
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/TimersRepositoriesModule.kt
@@ -0,0 +1,52 @@
+package org.timemates.backend.timers.deps.repositories
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.timers.data.PostgresqlTimerInvitesRepository
+import org.timemates.backend.timers.data.PostgresqlTimersRepository
+import org.timemates.backend.timers.data.cache.CacheTimersDataSource
+import org.timemates.backend.timers.data.db.TableTimerInvitesDataSource
+import org.timemates.backend.timers.data.db.TableTimerParticipantsDataSource
+import org.timemates.backend.timers.data.db.TableTimersDataSource
+import org.timemates.backend.timers.data.mappers.TimerInvitesMapper
+import org.timemates.backend.timers.data.mappers.TimersMapper
+import org.timemates.backend.timers.deps.repositories.database.TimersTablesModule
+import org.timemates.backend.timers.deps.repositories.mappers.TimersMapperModule
+import org.timemates.backend.timers.domain.repositories.TimerInvitesRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+
+@Module(
+ includes = [
+ TimersMapperModule::class,
+ TimersTablesModule::class,
+ ]
+)
+class TimersRepositoriesModule {
+ @Factory
+ fun timerInvitesRepo(
+ tableTimerInvitesDataSource: TableTimerInvitesDataSource,
+ participantsDataSource: TableTimerParticipantsDataSource,
+ invitesMapper: TimerInvitesMapper,
+ ): TimerInvitesRepository {
+ return PostgresqlTimerInvitesRepository(
+ tableTimerInvitesDataSource = tableTimerInvitesDataSource,
+ participantsDataSource = participantsDataSource,
+ invitesMapper = invitesMapper,
+ )
+ }
+
+ @Factory
+ fun timersRepo(
+ tableTimers: TableTimersDataSource,
+ cachedTimers: CacheTimersDataSource,
+ tableTimerParticipants: TableTimerParticipantsDataSource,
+ timersMapper: TimersMapper,
+ ): TimersRepository {
+ return PostgresqlTimersRepository(
+ tableTimers,
+ cachedTimers,
+ tableTimerParticipants,
+ timersMapper,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/TimersSessionsRepositoriesModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/TimersSessionsRepositoriesModule.kt
new file mode 100644
index 00000000..396c8045
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/TimersSessionsRepositoriesModule.kt
@@ -0,0 +1,26 @@
+package org.timemates.backend.timers.deps.repositories
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.timers.data.PostgresqlTimerSessionRepository
+import org.timemates.backend.timers.data.db.TableTimersSessionUsersDataSource
+import org.timemates.backend.timers.deps.repositories.cache.CacheTimersModule
+import org.timemates.backend.timers.deps.repositories.database.TimersTablesModule
+import org.timemates.backend.timers.deps.repositories.mappers.TimersMapperModule
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+
+@Module(
+ includes = [
+ TimersTablesModule::class,
+ CacheTimersModule::class,
+ TimersMapperModule::class,
+ ]
+)
+class TimersSessionsRepositoriesModule {
+ @Factory
+ fun sessionsRepository(
+ tableTimersSessionUsers: TableTimersSessionUsersDataSource,
+ ): TimerSessionRepository {
+ return PostgresqlTimerSessionRepository(tableTimersSessionUsers = tableTimersSessionUsers)
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/cache/CacheTimersModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/cache/CacheTimersModule.kt
new file mode 100644
index 00000000..cf25cca9
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/cache/CacheTimersModule.kt
@@ -0,0 +1,16 @@
+package org.timemates.backend.timers.deps.repositories.cache
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Named
+import org.timemates.backend.timers.data.cache.CacheTimersDataSource
+
+@Module
+class CacheTimersModule {
+ @Factory
+ fun cacheTimersDs(
+ @Named("timers.cache.size") maxSize: Long,
+ ): CacheTimersDataSource {
+ return CacheTimersDataSource(maxSize)
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/database/TimersTablesModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/database/TimersTablesModule.kt
new file mode 100644
index 00000000..e8aa666c
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/database/TimersTablesModule.kt
@@ -0,0 +1,52 @@
+package org.timemates.backend.timers.deps.repositories.database
+
+import org.jetbrains.exposed.sql.Database
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.timers.data.db.*
+import org.timemates.backend.timers.data.mappers.TimerInvitesMapper
+import org.timemates.backend.timers.data.mappers.TimerSessionMapper
+import org.timemates.backend.timers.data.mappers.TimersMapper
+import org.timemates.backend.timers.deps.repositories.mappers.TimersMapperModule
+
+@Module(includes = [TimersMapperModule::class])
+class TimersTablesModule {
+ @Factory
+ fun timerInvitesDs(
+ database: Database,
+ invitesMapper: TimerInvitesMapper,
+ ): TableTimerInvitesDataSource {
+ return TableTimerInvitesDataSource(database, invitesMapper)
+ }
+
+ @Factory
+ fun timerParticipantsDs(
+ database: Database,
+ ): TableTimerParticipantsDataSource {
+ return TableTimerParticipantsDataSource(database)
+ }
+
+ @Factory
+ fun timersDs(
+ database: Database,
+ timersMapper: TimersMapper,
+ ): TableTimersDataSource {
+ return TableTimersDataSource(database, timersMapper)
+ }
+
+ @Factory
+ fun timerSessionUsersDs(
+ database: Database,
+ timerSessionMapper: TimerSessionMapper,
+ ): TableTimersSessionUsersDataSource {
+ return TableTimersSessionUsersDataSource(database, timerSessionMapper)
+ }
+
+ @Factory
+ fun tableTimersStateDs(
+ database: Database,
+ timersMapper: TimersMapper,
+ ): TableTimersStateDataSource {
+ return TableTimersStateDataSource(database, timersMapper)
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/mappers/TimersMapperModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/mappers/TimersMapperModule.kt
new file mode 100644
index 00000000..f159a2a9
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/repositories/mappers/TimersMapperModule.kt
@@ -0,0 +1,20 @@
+package org.timemates.backend.timers.deps.repositories.mappers
+
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.koin.core.annotation.Singleton
+import org.timemates.backend.timers.data.mappers.TimerInvitesMapper
+import org.timemates.backend.timers.data.mappers.TimerSessionMapper
+import org.timemates.backend.timers.data.mappers.TimersMapper
+
+@Module
+class TimersMapperModule {
+ @Singleton
+ fun timerInvitesMapper(): TimerInvitesMapper = TimerInvitesMapper()
+
+ @Singleton
+ fun timerSessionsMapper(): TimerSessionMapper = TimerSessionMapper()
+
+ @Factory
+ fun timersMapper(sessionMapper: TimerSessionMapper): TimersMapper = TimersMapper(sessionMapper)
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerInvitesUseCasesModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerInvitesUseCasesModule.kt
new file mode 100644
index 00000000..2a196317
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerInvitesUseCasesModule.kt
@@ -0,0 +1,68 @@
+package org.timemates.backend.timers.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import com.timemates.random.RandomProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.repositories.TimerInvitesRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.usecases.members.invites.CreateInviteUseCase
+import org.timemates.backend.timers.domain.usecases.members.invites.GetInvitesUseCase
+import org.timemates.backend.timers.domain.usecases.members.invites.JoinByInviteUseCase
+import org.timemates.backend.timers.domain.usecases.members.invites.RemoveInviteUseCase
+
+@Module
+class TimerInvitesUseCasesModule {
+ @Factory
+ fun createInviteUseCase(
+ invites: TimerInvitesRepository,
+ timers: TimersRepository,
+ randomProvider: RandomProvider,
+ timeProvider: TimeProvider,
+ ): CreateInviteUseCase {
+ return CreateInviteUseCase(
+ invites = invites,
+ timers = timers,
+ randomProvider = randomProvider,
+ timeProvider = timeProvider,
+ )
+ }
+
+ @Factory
+ fun getInvitesUseCase(
+ invites: TimerInvitesRepository,
+ timers: TimersRepository,
+ ): GetInvitesUseCase {
+ return GetInvitesUseCase(
+ invites = invites,
+ timers = timers,
+ )
+ }
+
+ @Factory
+ fun joinByInviteUseCase(
+ invites: TimerInvitesRepository,
+ timers: TimersRepository,
+ timeProvider: TimeProvider,
+ timersStateMachine: TimersStateMachine,
+ ): JoinByInviteUseCase {
+ return JoinByInviteUseCase(
+ invites = invites,
+ timers = timers,
+ time = timeProvider,
+ fsm = timersStateMachine,
+ )
+ }
+
+ @Factory
+ fun removeInviteUseCase(
+ invites: TimerInvitesRepository,
+ timers: TimersRepository,
+ ): RemoveInviteUseCase {
+ return RemoveInviteUseCase(
+ invites = invites,
+ timers = timers,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerMembersUseCasesModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerMembersUseCasesModule.kt
new file mode 100644
index 00000000..92f383a0
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerMembersUseCasesModule.kt
@@ -0,0 +1,49 @@
+package org.timemates.backend.timers.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.repositories.UsersRepository
+import org.timemates.backend.timers.domain.usecases.members.GetMembersInSessionUseCase
+import org.timemates.backend.timers.domain.usecases.members.GetMembersUseCase
+import org.timemates.backend.timers.domain.usecases.members.KickTimerUserUseCase
+
+@Module
+class TimerMembersUseCasesModule {
+ @Factory
+ fun getMembersInSession(
+ timersRepository: TimersRepository,
+ sessionsRepository: TimerSessionRepository,
+ usersRepository: UsersRepository,
+ timeProvider: TimeProvider,
+ ): GetMembersInSessionUseCase {
+ return GetMembersInSessionUseCase(
+ timersRepository,
+ sessionsRepository,
+ usersRepository,
+ timeProvider,
+ )
+ }
+
+ @Factory
+ fun getMembers(
+ timersRepository: TimersRepository,
+ usersRepository: UsersRepository,
+ ): GetMembersUseCase {
+ return GetMembersUseCase(
+ timersRepository,
+ usersRepository,
+ )
+ }
+
+ @Factory
+ fun kickMember(
+ timersRepository: TimersRepository,
+ ): KickTimerUserUseCase {
+ return KickTimerUserUseCase(
+ timersRepository,
+ )
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerSessionsUseCaseModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerSessionsUseCaseModule.kt
new file mode 100644
index 00000000..3dcbd831
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimerSessionsUseCaseModule.kt
@@ -0,0 +1,66 @@
+package org.timemates.backend.timers.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.usecases.sessions.*
+
+@Module
+class TimerSessionsUseCaseModule {
+ @Factory
+ fun confirmStartUseCase(
+ fsm: TimersStateMachine,
+ sessions: TimerSessionRepository,
+ timeProvider: TimeProvider,
+ ): ConfirmStartUseCase {
+ return ConfirmStartUseCase(fsm, sessions, timeProvider)
+ }
+
+ @Factory
+ fun getCurrentTimerSessionUseCase(
+ fsm: TimersStateMachine,
+ sessions: TimerSessionRepository,
+ timers: TimersRepository,
+ timeProvider: TimeProvider,
+ ): GetCurrentTimerSessionUseCase {
+ return GetCurrentTimerSessionUseCase(fsm, sessions, timers, timeProvider)
+ }
+
+ @Factory
+ fun getStateUpdatesUseCase(
+ fsm: TimersStateMachine,
+ timers: TimersRepository,
+ ): GetStateUpdatesUseCase {
+ return GetStateUpdatesUseCase(timers, fsm)
+ }
+
+ @Factory
+ fun joinSessionUseCase(
+ fsm: TimersStateMachine,
+ sessions: TimerSessionRepository,
+ timers: TimersRepository,
+ timeProvider: TimeProvider,
+ ): JoinSessionUseCase {
+ return JoinSessionUseCase(timers, fsm, sessions, timeProvider)
+ }
+
+ @Factory
+ fun leaveSessionUseCase(
+ fsm: TimersStateMachine,
+ sessions: TimerSessionRepository,
+ timeProvider: TimeProvider,
+ ): LeaveSessionUseCase {
+ return LeaveSessionUseCase(sessions, fsm, timeProvider)
+ }
+
+ @Factory
+ fun pingSessionUseCase(
+ sessions: TimerSessionRepository,
+ timeProvider: TimeProvider,
+ ): PingSessionUseCase {
+ return PingSessionUseCase(sessions, timeProvider)
+ }
+}
\ No newline at end of file
diff --git a/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimersUseCasesModule.kt b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimersUseCasesModule.kt
new file mode 100644
index 00000000..32390578
--- /dev/null
+++ b/features/timers/dependencies/src/main/kotlin/org/timemates/backend/timers/deps/usecases/TimersUseCasesModule.kt
@@ -0,0 +1,79 @@
+package org.timemates.backend.timers.deps.usecases
+
+import com.timemates.backend.time.TimeProvider
+import org.koin.core.annotation.Factory
+import org.koin.core.annotation.Module
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.usecases.*
+
+@Module
+class TimersUseCasesModule {
+ @Factory
+ fun createTimerUseCase(
+ timers: TimersRepository,
+ time: TimeProvider,
+ ): CreateTimerUseCase {
+ return CreateTimerUseCase(timers, time)
+ }
+
+ @Factory
+ fun getTimersUseCase(
+ timers: TimersRepository,
+ fsm: TimersStateMachine,
+ ): GetTimersUseCase {
+ return GetTimersUseCase(timers, fsm)
+ }
+
+ @Factory
+ fun getTimerUseCase(
+ timers: TimersRepository,
+ fsm: TimersStateMachine,
+ ): GetTimerUseCase {
+ return GetTimerUseCase(timers, fsm)
+ }
+
+ @Factory
+ fun leaveTimerUseCase(
+ timers: TimersRepository,
+ ): LeaveTimerUseCase {
+ return LeaveTimerUseCase(timers)
+ }
+
+ @Factory
+ fun removeTimerUseCase(
+ timers: TimersRepository,
+ ): RemoveTimerUseCase {
+ return RemoveTimerUseCase(timers)
+ }
+
+ @Factory
+ fun setTimerInfoUseCase(
+ timers: TimersRepository,
+ ): SetTimerInfoUseCase {
+ return SetTimerInfoUseCase(timers)
+ }
+
+ @Factory
+ fun setTimerSettingsUseCase(
+ timers: TimersRepository,
+ ): SetTimerSettingsUseCase {
+ return SetTimerSettingsUseCase(timers)
+ }
+
+ @Factory
+ fun startTimerUseCase(
+ timers: TimersRepository,
+ fsm: TimersStateMachine,
+ ): StartTimerUseCase {
+ return StartTimerUseCase(timers, fsm)
+ }
+
+ @Factory
+ fun stopTimerUseCase(
+ timers: TimersRepository,
+ fsm: TimersStateMachine,
+ ): StopTimerUseCase {
+ return StopTimerUseCase(timers, fsm)
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/build.gradle.kts b/features/timers/domain/build.gradle.kts
new file mode 100644
index 00000000..5993a00f
--- /dev/null
+++ b/features/timers/domain/build.gradle.kts
@@ -0,0 +1,19 @@
+plugins {
+ id(libs.plugins.jvm.module.convention.get().pluginId)
+}
+
+dependencies {
+ implementation(projects.foundation.time)
+ implementation(projects.foundation.pageToken)
+ implementation(projects.foundation.random)
+
+ implementation(libs.kotlinx.coroutines)
+ implementation(projects.core.types.authIntegration)
+ api(projects.core.types)
+
+ api(projects.foundation.stateMachine)
+}
+
+tasks.withType {
+ useJUnitPlatform()
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/fsm/TimersStateMachine.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/fsm/TimersStateMachine.kt
new file mode 100644
index 00000000..68043245
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/fsm/TimersStateMachine.kt
@@ -0,0 +1,113 @@
+package org.timemates.backend.timers.domain.fsm
+
+import com.timemates.backend.time.TimeProvider
+import com.timemates.backend.time.UnixTime
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.first
+import org.timemates.backend.fsm.StateMachine
+import org.timemates.backend.fsm.StateStorage
+import org.timemates.backend.fsm.stateMachineController
+import org.timemates.backend.fsm.toStateMachine
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimerEvent
+import org.timemates.backend.types.timers.TimerState
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.value.UserId
+import kotlin.time.Duration.Companion.minutes
+import kotlin.time.Duration.Companion.seconds
+
+class TimersStateMachine(
+ timers: TimersRepository,
+ sessions: TimerSessionRepository,
+ storage: StateStorage,
+ timeProvider: TimeProvider,
+ coroutineScope: CoroutineScope,
+) : StateMachine by stateMachineController({
+ state(TimerState.Inactive, TimerState.Paused, TimerState.Rest) {
+ onEvent { timerId, state, event ->
+ when (event) {
+ TimerEvent.Start -> {
+ val currentTime = timeProvider.provide()
+ val settings = timers.getTimerSettings(timerId)!!
+
+ if (settings.isConfirmationRequired) {
+ sessions.setActiveUsersConfirmationRequirement(timerId)
+ TimerState.ConfirmationWaiting(currentTime, 30.seconds)
+ } else TimerState.Running(currentTime, settings.workTime)
+ }
+
+ else -> state
+ }
+ }
+
+ onTimeout { timerId, state ->
+ when (state) {
+ is TimerState.Rest -> TimerState.Running(
+ timeProvider.provide(),
+ timers.getTimerSettings(timerId)!!.workTime,
+ )
+
+ else -> TimerState.Inactive(timeProvider.provide())
+ }
+ }
+ }
+
+ state(TimerState.Running) {
+ onEvent { _, state, event ->
+ when (event) {
+ TimerEvent.Stop -> TimerState.Paused(timeProvider.provide())
+ else -> state
+ }
+ }
+
+ onTimeout { timerId, _ ->
+ TimerState.Rest(timeProvider.provide(), timers.getTimerSettings(timerId)!!.restTime)
+ }
+ }
+
+ state(TimerState.ConfirmationWaiting) {
+ onEvent { timerId, state, event: TimerEvent ->
+ when (event) {
+ is TimerEvent.AttendanceConfirmed -> {
+ if (sessions.markConfirmed(timerId, event.userId, timeProvider.provide()))
+ TimerState.Running(timeProvider.provide(), timers.getTimerSettings(timerId)!!.workTime)
+ state
+ }
+
+ else -> state
+ }
+ }
+
+ onTimeout { timerId, _ ->
+ sessions.removeNotConfirmedUsers(timerId)
+ val time = timeProvider.provide()
+ if (sessions.getMembersCount(timerId, time - 15.minutes).int > 0) {
+ TimerState.Running(timeProvider.provide(), timers.getTimerSettings(timerId)!!.workTime)
+ } else TimerState.Inactive(time)
+ }
+ }
+}).toStateMachine(storage, timeProvider, coroutineScope)
+
+suspend fun TimersStateMachine.getCurrentState(timerId: TimerId): TimerState =
+ getState(timerId).first()
+
+
+suspend fun TimersStateMachine.isConfirmationState(timerId: TimerId): Boolean =
+ getCurrentState(timerId) is TimerState.ConfirmationWaiting
+
+suspend fun TimersStateMachine.isRunningState(timerId: TimerId): Boolean =
+ getCurrentState(timerId) is TimerState.Running
+
+suspend fun TimersStateMachine.canStart(timerId: TimerId): Boolean {
+ val state = getCurrentState(timerId)
+ return state !is TimerState.Running && state !is TimerState.ConfirmationWaiting
+}
+
+suspend fun TimersStateMachine.isPauseState(timerId: TimerId): Boolean =
+ getCurrentState(timerId) is TimerState.Paused
+
+suspend fun TimerSessionRepository.hasSession(
+ userId: UserId,
+ lastActiveTime: UnixTime,
+): Boolean = getTimerIdOfCurrentSession(userId, lastActiveTime) != null
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimerInvitesRepository.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimerInvitesRepository.kt
new file mode 100644
index 00000000..e6711d4e
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimerInvitesRepository.kt
@@ -0,0 +1,26 @@
+package org.timemates.backend.timers.domain.repositories
+
+import com.timemates.backend.time.UnixTime
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.Invite
+import org.timemates.backend.types.timers.value.InviteCode
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.value.UserId
+
+interface TimerInvitesRepository {
+ suspend fun getInvites(timerId: TimerId, nextPageToken: PageToken?): Page
+ suspend fun removeInvite(timerId: TimerId, code: InviteCode)
+ suspend fun getInvite(code: InviteCode): Invite?
+
+ suspend fun getInvitesCount(timerId: TimerId, after: UnixTime): Count
+
+ suspend fun createInvite(
+ timerId: TimerId,
+ userId: UserId,
+ code: InviteCode,
+ creationTime: UnixTime,
+ limit: Count,
+ )
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/repositories/TimerSessionRepository.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimerSessionRepository.kt
similarity index 68%
rename from core/src/main/kotlin/io/timemates/backend/timers/repositories/TimerSessionRepository.kt
rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimerSessionRepository.kt
index f4cdfb06..ef920579 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/repositories/TimerSessionRepository.kt
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimerSessionRepository.kt
@@ -1,19 +1,13 @@
-package io.timemates.backend.timers.repositories
+package org.timemates.backend.timers.domain.repositories
import com.timemates.backend.time.UnixTime
-import io.timemates.backend.common.markers.Repository
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.fsm.getCurrentState
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.timers.fsm.ConfirmationState
-import io.timemates.backend.timers.fsm.PauseState
-import io.timemates.backend.timers.fsm.RunningState
-import io.timemates.backend.timers.fsm.TimersStateMachine
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.UserId
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.value.UserId
-interface TimerSessionRepository : TimersStateMachine, Repository {
+interface TimerSessionRepository {
/**
* Adds user to the session of a timer.
*
@@ -32,18 +26,18 @@ interface TimerSessionRepository : TimersStateMachine, Repository {
suspend fun removeUser(timerId: TimerId, userId: UserId)
/**
- * Get a id of the timer of current session.
- *
+ * Get an id of the timer of current session.
+ *
* @param userId The id of the user that joins.
* @param lastActiveTime The Unix time at which the user joined the timer session.
- *
+ *
* @return [TimerId] of a session where's the user joined or `null`.
*/
suspend fun getTimerIdOfCurrentSession(userId: UserId, lastActiveTime: UnixTime): TimerId?
/**
* Gets members of a session.
- *
+ *
* @param timerId The id of the timer.
* @param pageToken A page token that identifies a specific page.
* @param lastActiveTime The Unix time at which the user joined the timer session.
@@ -56,7 +50,7 @@ interface TimerSessionRepository : TimersStateMachine, Repository {
/**
* Get count of members in a session.
- *
+ *
* @param timerId The id of the timer.
* @param activeAfterTime The Unix time at which the user joined the timer session.
*/
@@ -66,7 +60,7 @@ interface TimerSessionRepository : TimersStateMachine, Repository {
* Sets confirmation requirement of a timer to users that are currently active. We
* do not apply it to all users as users that joined after should be automatically marked as
* confirmed.
- *
+ *
* @param timerId The id of the timer.
*/
suspend fun setActiveUsersConfirmationRequirement(timerId: TimerId)
@@ -84,7 +78,7 @@ interface TimerSessionRepository : TimersStateMachine, Repository {
/**
* Removes all users that didn't have activity after given [afterTime].
- *
+ *
* @param afterTime The Unix time of last active time after which all users will be removed.
*/
suspend fun removeInactiveUsers(afterTime: UnixTime)
@@ -99,30 +93,10 @@ interface TimerSessionRepository : TimersStateMachine, Repository {
/**
* Updates last user activity time in session. It should be done every 5-10 minutes by the client.
- *
+ *
* @param timerId The id of the timer.
* @param userId The id of the user that confirms his attendance.
* @param time The Unix time of last user activity.
*/
suspend fun updateLastActivityTime(timerId: TimerId, userId: UserId, time: UnixTime)
-}
-
-
-suspend fun TimerSessionRepository.isConfirmationState(timerId: TimerId): Boolean =
- getCurrentState(timerId) is ConfirmationState
-
-suspend fun TimerSessionRepository.isRunningState(timerId: TimerId): Boolean =
- getCurrentState(timerId) is RunningState
-
-suspend fun TimerSessionRepository.canStart(timerId: TimerId): Boolean {
- val state = getCurrentState(timerId)
- return state !is RunningState && state !is ConfirmationState
-}
-
-suspend fun TimerSessionRepository.isPauseState(timerId: TimerId): Boolean =
- getCurrentState(timerId) is PauseState
-
-suspend fun TimerSessionRepository.hasSession(
- userId: UserId,
- lastActiveTime: UnixTime,
-): Boolean = getTimerIdOfCurrentSession(userId, lastActiveTime) != null
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/repositories/TimersRepository.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimersRepository.kt
similarity index 67%
rename from core/src/main/kotlin/io/timemates/backend/timers/repositories/TimersRepository.kt
rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimersRepository.kt
index 9b4f5149..7bc0244f 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/repositories/TimersRepository.kt
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/TimersRepository.kt
@@ -1,18 +1,19 @@
-package io.timemates.backend.timers.repositories
+package org.timemates.backend.timers.domain.repositories
import com.timemates.backend.time.UnixTime
-import io.timemates.backend.common.markers.Repository
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.pagination.Page
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.timers.types.TimerSettings
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.timers.types.value.TimerDescription
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.timers.types.value.TimerName
-import io.timemates.backend.users.types.value.UserId
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.Timer
+import org.timemates.backend.types.timers.TimerSettings
+import org.timemates.backend.types.timers.TimerState
+import org.timemates.backend.types.timers.value.InviteCode
+import org.timemates.backend.types.timers.value.TimerDescription
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.timers.value.TimerName
+import org.timemates.backend.types.users.value.UserId
-interface TimersRepository : Repository {
+interface TimersRepository {
suspend fun createTimer(
name: TimerName,
description: TimerDescription,
@@ -72,4 +73,8 @@ interface TimersRepository : Repository {
val description: TimerDescription? = null,
)
}
+}
+
+fun TimersRepository.TimerInformation.toTimer(state: TimerState): Timer {
+ return Timer(id, name, description, ownerId, settings, membersCount, state)
}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/UsersRepository.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/UsersRepository.kt
new file mode 100644
index 00000000..6389b5ec
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/repositories/UsersRepository.kt
@@ -0,0 +1,14 @@
+package org.timemates.backend.timers.domain.repositories
+
+import org.timemates.backend.types.users.User
+import org.timemates.backend.types.users.value.UserId
+
+interface UsersRepository {
+
+ /**
+ * Gets users by [userIds].
+ *
+ * @return [List] of users where order to [userIds] is guaranteed.
+ */
+ suspend fun getUsers(userIds: List): List
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/CreateTimerUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/CreateTimerUseCase.kt
similarity index 51%
rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/CreateTimerUseCase.kt
rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/CreateTimerUseCase.kt
index 5d915bd9..fd2309e6 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/CreateTimerUseCase.kt
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/CreateTimerUseCase.kt
@@ -1,33 +1,29 @@
-package io.timemates.backend.timers.usecases
+package org.timemates.backend.timers.domain.usecases
import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerSettings
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerDescription
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.timers.types.value.TimerName
-import io.timemates.backend.users.types.value.userId
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimerSettings
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerDescription
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.timers.value.TimerName
import kotlin.time.Duration.Companion.minutes
class CreateTimerUseCase(
private val timers: TimersRepository,
private val time: TimeProvider,
-) : UseCase {
- context(AuthorizedContext)
+) {
suspend fun execute(
+ auth: Authorized,
name: TimerName,
description: TimerDescription,
settings: TimerSettings,
): Result {
+ val userId = auth.userId
val currentTime = time.provide()
- return if (
- timers.getOwnedTimersCount(
- userId, after = currentTime - 30.minutes
- ) > 20
- ) {
+ return if (timers.getOwnedTimersCount(userId, after = currentTime - 30.minutes) > 20) {
Result.TooManyCreations
} else {
val timerId = timers.createTimer(name, description, settings, userId, time.provide())
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/GetTimerUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/GetTimerUseCase.kt
new file mode 100644
index 00000000..83522823
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/GetTimerUseCase.kt
@@ -0,0 +1,38 @@
+package org.timemates.backend.timers.domain.usecases
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.fsm.getCurrentState
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.repositories.toTimer
+import org.timemates.backend.types.timers.Timer
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+
+class GetTimerUseCase(
+ private val timers: TimersRepository,
+ private val fsm: TimersStateMachine,
+) {
+
+ suspend fun execute(
+ auth: Authorized,
+ timerId: TimerId,
+ ): Result {
+ return if (timers.isMemberOf(auth.userId, timerId)) {
+ val timer = timers.getTimerInformation(timerId)
+ ?.toTimer(fsm.getCurrentState(timerId))
+ ?: return Result.NotFound
+
+ Result.Success(timer)
+ } else {
+ Result.NotFound
+ }
+ }
+
+ sealed interface Result {
+ @JvmInline
+ value class Success(val timer: Timer) : Result
+ data object NotFound : Result
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/GetTimersUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/GetTimersUseCase.kt
new file mode 100644
index 00000000..2a695277
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/GetTimersUseCase.kt
@@ -0,0 +1,46 @@
+package org.timemates.backend.timers.domain.usecases
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.pagination.map
+import org.timemates.backend.pagination.mapIndexed
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.fsm.getCurrentState
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.repositories.toTimer
+import org.timemates.backend.types.timers.Timer
+import org.timemates.backend.types.timers.TimersScope
+
+class GetTimersUseCase(
+ private val timers: TimersRepository,
+ private val fsm: TimersStateMachine,
+) {
+
+ suspend fun execute(
+ auth: Authorized,
+ nextPageToken: PageToken?,
+ ): Result {
+ val infos = timers.getTimersInformation(
+ auth.userId, nextPageToken,
+ )
+
+ val ids = infos.map(TimersRepository.TimerInformation::id)
+ val states = ids.map { id -> fsm.getCurrentState(id) }
+
+ return Result.Success(
+ infos.mapIndexed { index, information ->
+ information.toTimer(
+ states.value[index]
+ )
+ },
+ )
+ }
+
+ sealed interface Result {
+ data class Success(
+ val page: Page,
+ ) : Result
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/LeaveTimerUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/LeaveTimerUseCase.kt
new file mode 100644
index 00000000..067b72eb
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/LeaveTimerUseCase.kt
@@ -0,0 +1,23 @@
+package org.timemates.backend.timers.domain.usecases
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+
+class LeaveTimerUseCase(
+ private val timersRepository: TimersRepository,
+) {
+ suspend fun execute(
+ auth: Authorized,
+ timerId: TimerId,
+ ): Result {
+ timersRepository.removeMember(auth.userId, timerId)
+ return Result.Success
+ }
+
+ sealed interface Result {
+ data object Success : Result
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/RemoveTimerUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/RemoveTimerUseCase.kt
new file mode 100644
index 00000000..f1263aca
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/RemoveTimerUseCase.kt
@@ -0,0 +1,28 @@
+package org.timemates.backend.timers.domain.usecases
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+
+class RemoveTimerUseCase(
+ private val timers: TimersRepository,
+) {
+ suspend fun execute(auth: Authorized, timerId: TimerId): Result {
+ val timer = timers.getTimerInformation(timerId)
+ return when {
+ timer == null || timer.ownerId != auth.userId -> Result.NotFound
+ else -> {
+ timers.removeTimer(timerId)
+ Result.Success
+ }
+ }
+
+ }
+
+ sealed interface Result {
+ data object Success : Result
+ data object NotFound : Result
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/SetTimerInfoUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/SetTimerInfoUseCase.kt
similarity index 54%
rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/SetTimerInfoUseCase.kt
rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/SetTimerInfoUseCase.kt
index 63e31774..00bcf00e 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/SetTimerInfoUseCase.kt
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/SetTimerInfoUseCase.kt
@@ -1,25 +1,24 @@
-package io.timemates.backend.timers.usecases
+package org.timemates.backend.timers.domain.usecases
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimerSettings
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimerSettings
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
class SetTimerInfoUseCase(
private val timers: TimersRepository,
-) : UseCase {
- context(AuthorizedContext)
+) {
suspend fun execute(
+ auth: Authorized,
timerId: TimerId,
patch: TimersRepository.TimerInformation.Patch,
newSettings: TimerSettings.Patch?,
): Result {
val info = timers.getTimerInformation(timerId) ?: return Result.NotFound
- if (info.ownerId != userId)
+ if (info.ownerId != auth.userId)
return Result.NoAccess
timers.setTimerInformation(timerId, patch)
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/SetTimerSettingsUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/SetTimerSettingsUseCase.kt
new file mode 100644
index 00000000..72aaf7fb
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/SetTimerSettingsUseCase.kt
@@ -0,0 +1,34 @@
+package org.timemates.backend.timers.domain.usecases
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimerSettings
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+
+class SetTimerSettingsUseCase(
+ private val timers: TimersRepository,
+) {
+
+ suspend fun execute(
+ auth: Authorized,
+ timerId: TimerId,
+ newSettings: TimerSettings.Patch,
+ ): Result {
+ if (timers.getTimerInformation(timerId)?.ownerId != auth.userId)
+ return Result.NoAccess
+
+ timers.setTimerSettings(
+ timerId,
+ newSettings
+ )
+
+ return Result.Success
+ }
+
+ sealed interface Result {
+ data object Success : Result
+ data object NoAccess : Result
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/StartTimerUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/StartTimerUseCase.kt
new file mode 100644
index 00000000..4d03ebf3
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/StartTimerUseCase.kt
@@ -0,0 +1,43 @@
+package org.timemates.backend.timers.domain.usecases
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.fsm.canStart
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimerEvent
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+
+class StartTimerUseCase(
+ private val timers: TimersRepository,
+ private val fsm: TimersStateMachine,
+) {
+ suspend fun execute(auth: Authorized, timerId: TimerId): Result {
+ val userId = auth.userId
+ val timer = timers.getTimerInformation(timerId) ?: return Result.NoAccess
+ val settings = timers.getTimerSettings(timerId)!!
+ return if (
+ (timer.ownerId == userId)
+ || (settings.isEveryoneCanPause && timers.isMemberOf(userId, timerId))
+ ) {
+ if (fsm.canStart(timerId)) {
+ fsm.sendEvent(timerId, TimerEvent.Start)
+ Result.Success
+ } else Result.WrongState
+ } else {
+ Result.NoAccess
+ }
+ }
+
+ sealed interface Result {
+ /**
+ * Denotes that the operation was failed due to invalid
+ * state that cannot perform the operation due to inconsistency.
+ */
+ data object WrongState : Result
+
+ data object Success : Result
+ data object NoAccess : Result
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/StopTimerUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/StopTimerUseCase.kt
new file mode 100644
index 00000000..65fcc468
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/StopTimerUseCase.kt
@@ -0,0 +1,45 @@
+package org.timemates.backend.timers.domain.usecases
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.fsm.isRunningState
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimerEvent
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+
+class StopTimerUseCase(
+ private val timers: TimersRepository,
+ private val fsm: TimersStateMachine,
+) {
+
+ suspend fun execute(
+ auth: Authorized,
+ timerId: TimerId,
+ ): Result {
+ val userId = auth.userId
+ val timer = timers.getTimerInformation(timerId) ?: return Result.NoAccess
+ val settings = timers.getTimerSettings(timerId)!!
+
+ return if (
+ (timer.ownerId == userId)
+ || (settings.isEveryoneCanPause && timers.isMemberOf(userId, timerId))
+ ) {
+ return if (fsm.isRunningState(timerId)) {
+ fsm.sendEvent(timerId, TimerEvent.Pause)
+ Result.Success
+ } else {
+ Result.WrongState
+ }
+ } else {
+ Result.NoAccess
+ }
+ }
+
+ sealed interface Result {
+ data object Success : Result
+ data object NoAccess : Result
+ data object WrongState : Result
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/GetMembersInSessionUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/GetMembersInSessionUseCase.kt
similarity index 50%
rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/members/GetMembersInSessionUseCase.kt
rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/GetMembersInSessionUseCase.kt
index fd33534b..e26f50cd 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/GetMembersInSessionUseCase.kt
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/GetMembersInSessionUseCase.kt
@@ -1,30 +1,29 @@
-package io.timemates.backend.timers.usecases.members
+package org.timemates.backend.timers.domain.usecases.members
-import com.timemates.backend.time.SystemTimeProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.timers.repositories.TimerSessionRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.repositories.UsersRepository
-import io.timemates.backend.users.types.User
-import io.timemates.backend.users.types.value.userId
+import com.timemates.backend.time.TimeProvider
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.repositories.UsersRepository
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.User
import kotlin.time.Duration.Companion.minutes
class GetMembersInSessionUseCase(
private val timersRepository: TimersRepository,
private val sessionsRepository: TimerSessionRepository,
private val usersRepository: UsersRepository,
- private val timeProvider: SystemTimeProvider,
-) : UseCase {
- context(AuthorizedContext)
+ private val timeProvider: TimeProvider,
+) {
suspend fun execute(
+ auth: Authorized,
timerId: TimerId,
pageToken: PageToken?,
): Result {
- if (!timersRepository.isMemberOf(userId, timerId))
+ if (!timersRepository.isMemberOf(auth.userId, timerId))
return Result.NoAccess
val userIds = sessionsRepository.getMembers(
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/GetMembersUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/GetMembersUseCase.kt
similarity index 51%
rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/members/GetMembersUseCase.kt
rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/GetMembersUseCase.kt
index 9e8ebf19..bfcdc0b1 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/GetMembersUseCase.kt
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/GetMembersUseCase.kt
@@ -1,25 +1,24 @@
-package io.timemates.backend.timers.usecases.members
+package org.timemates.backend.timers.domain.usecases.members
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.pagination.PageToken
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.repositories.UsersRepository
-import io.timemates.backend.users.types.User
-import io.timemates.backend.users.types.value.userId
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.repositories.UsersRepository
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.User
class GetMembersUseCase(
private val timersRepository: TimersRepository,
private val usersRepository: UsersRepository,
-) : UseCase {
- context(AuthorizedContext)
+) {
suspend fun execute(
+ auth: Authorized,
timerId: TimerId,
pageToken: PageToken?,
): Result {
- if (!timersRepository.isMemberOf(userId, timerId))
+ if (!timersRepository.isMemberOf(auth.userId, timerId))
return Result.NoAccess
val members = timersRepository.getMembers(
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/KickTimerUserUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/KickTimerUserUseCase.kt
new file mode 100644
index 00000000..b6636b64
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/KickTimerUserUseCase.kt
@@ -0,0 +1,29 @@
+package org.timemates.backend.timers.domain.usecases.members
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.types.users.value.UserId
+
+class KickTimerUserUseCase(
+ private val timersRepository: TimersRepository,
+) {
+ suspend fun execute(
+ auth: Authorized,
+ timerId: TimerId,
+ userToKick: UserId,
+ ): Result {
+ if (timersRepository.getTimerInformation(timerId)?.ownerId != auth.userId)
+ return Result.NoAccess
+
+ timersRepository.removeMember(userToKick, timerId)
+ return Result.Success
+ }
+
+ sealed interface Result {
+ data object Success : Result
+ data object NoAccess : Result
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/CreateInviteUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/CreateInviteUseCase.kt
similarity index 52%
rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/CreateInviteUseCase.kt
rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/CreateInviteUseCase.kt
index d37f3f52..988c934e 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/CreateInviteUseCase.kt
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/CreateInviteUseCase.kt
@@ -1,17 +1,17 @@
-package io.timemates.backend.timers.usecases.members.invites
+package org.timemates.backend.timers.domain.usecases.members.invites
import com.timemates.backend.time.TimeProvider
-import io.timemates.backend.validation.createOrThrowInternally
import com.timemates.random.RandomProvider
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.common.types.value.Count
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimerInvitesRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.repositories.TimerInvitesRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.common.value.Count
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.InviteCode
+import org.timemates.backend.types.timers.value.TimerId
+import org.timemates.backend.validation.annotations.ValidationDelicateApi
+import org.timemates.backend.validation.createUnsafe
import kotlin.time.Duration.Companion.minutes
class CreateInviteUseCase(
@@ -19,19 +19,23 @@ class CreateInviteUseCase(
private val timers: TimersRepository,
private val randomProvider: RandomProvider,
private val timeProvider: TimeProvider,
-) : UseCase {
- context(AuthorizedContext)
+) {
+
suspend fun execute(
+ auth: Authorized,
timerId: TimerId,
limit: Count,
): Result {
+ val userId = auth.userId
if (timers.getTimerInformation(timerId)?.ownerId != userId)
return Result.NoAccess
if (invites.getInvitesCount(timerId, timeProvider.provide() + 30.minutes).int > 10)
return Result.TooManyCreation
- val code = InviteCode.createOrThrowInternally(randomProvider.randomHash(InviteCode.SIZE))
+ @OptIn(ValidationDelicateApi::class)
+ val code = InviteCode.createUnsafe(randomProvider.randomHash(InviteCode.SIZE))
+
invites.createInvite(timerId, userId, code, timeProvider.provide(), limit)
return Result.Success(code)
}
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/GetInvitesUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/GetInvitesUseCase.kt
new file mode 100644
index 00000000..9138de54
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/GetInvitesUseCase.kt
@@ -0,0 +1,34 @@
+package org.timemates.backend.timers.domain.usecases.members.invites
+
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.pagination.Page
+import org.timemates.backend.pagination.PageToken
+import org.timemates.backend.timers.domain.repositories.TimerInvitesRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.Invite
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+
+class GetInvitesUseCase(
+ private val invites: TimerInvitesRepository,
+ private val timers: TimersRepository,
+) {
+
+ suspend fun execute(
+ auth: Authorized,
+ timerId: TimerId,
+ pageToken: PageToken?,
+ ): Result {
+ if (timers.getTimerInformation(timerId)?.ownerId != auth.userId)
+ return Result.NoAccess
+
+ return Result.Success(invites.getInvites(timerId, pageToken))
+ }
+
+ sealed interface Result {
+ @JvmInline
+ value class Success(val page: Page) : Result
+ data object NoAccess : Result
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/JoinByInviteUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/JoinByInviteUseCase.kt
new file mode 100644
index 00000000..8ff9b915
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/JoinByInviteUseCase.kt
@@ -0,0 +1,43 @@
+package org.timemates.backend.timers.domain.usecases.members.invites
+
+import com.timemates.backend.time.TimeProvider
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.fsm.getCurrentState
+import org.timemates.backend.timers.domain.repositories.TimerInvitesRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.repositories.toTimer
+import org.timemates.backend.types.timers.Timer
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.InviteCode
+
+class JoinByInviteUseCase(
+ private val invites: TimerInvitesRepository,
+ private val timers: TimersRepository,
+ private val time: TimeProvider,
+ private val fsm: TimersStateMachine,
+) {
+
+ suspend fun execute(
+ auth: Authorized,
+ code: InviteCode,
+ ): Result {
+ val invite = invites.getInvite(code) ?: return Result.NotFound
+ timers.addMember(auth.userId, invite.timerId, time.provide(), code)
+
+ if (invite.limit.int >= timers.getMembersCountOfInvite(invite.timerId, invite.code).int)
+ invites.removeInvite(invite.timerId, invite.code)
+
+ return Result.Success(
+ timers.getTimerInformation(invite.timerId)!!
+ .toTimer(fsm.getCurrentState(invite.timerId))
+ )
+ }
+
+ sealed interface Result {
+ @JvmInline
+ value class Success(val timer: Timer) : Result
+ data object NotFound : Result
+ }
+}
\ No newline at end of file
diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/RemoveInviteUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/RemoveInviteUseCase.kt
similarity index 50%
rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/RemoveInviteUseCase.kt
rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/RemoveInviteUseCase.kt
index ae192dca..872e4cd0 100644
--- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/members/invites/RemoveInviteUseCase.kt
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/members/invites/RemoveInviteUseCase.kt
@@ -1,25 +1,25 @@
-package io.timemates.backend.timers.usecases.members.invites
+package org.timemates.backend.timers.domain.usecases.members.invites
-import io.timemates.backend.common.markers.UseCase
-import io.timemates.backend.features.authorization.AuthorizedContext
-import io.timemates.backend.timers.repositories.TimerInvitesRepository
-import io.timemates.backend.timers.repositories.TimersRepository
-import io.timemates.backend.timers.types.TimersScope
-import io.timemates.backend.timers.types.value.InviteCode
-import io.timemates.backend.timers.types.value.TimerId
-import io.timemates.backend.users.types.value.userId
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.repositories.TimerInvitesRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.InviteCode
+import org.timemates.backend.types.timers.value.TimerId
class RemoveInviteUseCase(
private val invites: TimerInvitesRepository,
private val timers: TimersRepository,
-) : UseCase {
- context(AuthorizedContext)
+) {
+
suspend fun execute(
+ auth: Authorized,
timerId: TimerId,
code: InviteCode,
): Result {
val invite = invites.getInvite(code) ?: return Result.NotFound
- if (timers.getTimerInformation(invite.timerId)?.ownerId != userId)
+ if (timers.getTimerInformation(invite.timerId)?.ownerId != auth.userId)
return Result.NoAccess
invites.removeInvite(timerId, code)
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/ConfirmStartUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/ConfirmStartUseCase.kt
new file mode 100644
index 00000000..cbcb718b
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/ConfirmStartUseCase.kt
@@ -0,0 +1,38 @@
+package org.timemates.backend.timers.domain.usecases.sessions
+
+import com.timemates.backend.time.TimeProvider
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.fsm.isConfirmationState
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+import org.timemates.backend.types.timers.TimerEvent
+import org.timemates.backend.types.timers.TimersScope
+import kotlin.time.Duration.Companion.minutes
+
+class ConfirmStartUseCase(
+ private val fsm: TimersStateMachine,
+ private val sessions: TimerSessionRepository,
+ private val time: TimeProvider,
+) {
+
+ suspend fun execute(
+ auth: Authorized,
+ ): Result {
+ val userId = auth.userId
+ val timerId = sessions.getTimerIdOfCurrentSession(userId, time.provide() - 15.minutes)
+ ?: return Result.NotFound
+
+ if (!fsm.isConfirmationState(timerId))
+ return Result.WrongState
+
+ fsm.sendEvent(timerId, TimerEvent.AttendanceConfirmed(timerId, userId))
+ return Result.Success
+ }
+
+ sealed interface Result {
+ data object WrongState : Result
+ data object NotFound : Result
+ data object Success : Result
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/GetCurrentTimerSessionUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/GetCurrentTimerSessionUseCase.kt
new file mode 100644
index 00000000..577e45e5
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/GetCurrentTimerSessionUseCase.kt
@@ -0,0 +1,43 @@
+package org.timemates.backend.timers.domain.usecases.sessions
+
+import com.timemates.backend.time.TimeProvider
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.fsm.getCurrentState
+import org.timemates.backend.timers.domain.repositories.TimerSessionRepository
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.timers.domain.repositories.toTimer
+import org.timemates.backend.types.timers.Timer
+import org.timemates.backend.types.timers.TimersScope
+import kotlin.time.Duration.Companion.minutes
+
+class GetCurrentTimerSessionUseCase(
+ private val fsm: TimersStateMachine,
+ private val sessionsRepository: TimerSessionRepository,
+ private val timers: TimersRepository,
+ private val time: TimeProvider,
+) {
+
+ suspend fun execute(auth: Authorized): Result {
+ return when (val id = sessionsRepository.getTimerIdOfCurrentSession(auth.userId, time.provide() - 15.minutes)) {
+ null -> Result.NotFound
+
+ else -> {
+ Result.Success(
+ timers.getTimerInformation(id)!!.toTimer(
+ fsm.getCurrentState(id)
+ )
+ )
+ }
+ }
+ }
+
+ sealed interface Result {
+ data class Success(
+ val timer: Timer,
+ ) : Result
+
+ data object NotFound : Result
+ }
+}
\ No newline at end of file
diff --git a/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/GetStateUpdatesUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/GetStateUpdatesUseCase.kt
new file mode 100644
index 00000000..8624cd76
--- /dev/null
+++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/GetStateUpdatesUseCase.kt
@@ -0,0 +1,29 @@
+package org.timemates.backend.timers.domain.usecases.sessions
+
+import kotlinx.coroutines.flow.Flow
+import org.timemates.backend.core.types.integration.auth.userId
+import org.timemates.backend.foundation.authorization.Authorized
+import org.timemates.backend.timers.domain.fsm.TimersStateMachine
+import org.timemates.backend.timers.domain.repositories.TimersRepository
+import org.timemates.backend.types.timers.TimerState
+import org.timemates.backend.types.timers.TimersScope
+import org.timemates.backend.types.timers.value.TimerId
+
+class GetStateUpdatesUseCase(
+ private val timersRepository: TimersRepository,
+ private val fsm: TimersStateMachine,
+) {
+
+ suspend fun execute(auth: Authorized, timerId: TimerId): Result {
+ if (!timersRepository.isMemberOf(auth.userId, timerId))
+ return Result.NoAccess
+
+ return Result.Success(fsm.getState(timerId))
+ }
+
+ sealed interface Result {
+ @JvmInline
+ value class Success(val states: Flow