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) : Result + data object NoAccess : Result + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/JoinSessionUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/JoinSessionUseCase.kt similarity index 50% rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/JoinSessionUseCase.kt rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/JoinSessionUseCase.kt index 49588993..8d042cc9 100644 --- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/JoinSessionUseCase.kt +++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/JoinSessionUseCase.kt @@ -1,26 +1,29 @@ -package io.timemates.backend.timers.usecases.sessions +package org.timemates.backend.timers.domain.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.hasSession -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 +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.hasSession +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.TimersScope +import org.timemates.backend.types.timers.value.TimerId import kotlin.time.Duration.Companion.minutes class JoinSessionUseCase( private val timers: TimersRepository, + private val fsm: TimersStateMachine, private val sessions: TimerSessionRepository, private val time: TimeProvider, -) : UseCase { - context(AuthorizedContext) +) { + suspend fun execute( + auth: Authorized, timerId: TimerId, ): Result { + val userId = auth.userId val lastActiveTime = time.provide() - 15.minutes return when { @@ -28,7 +31,7 @@ class JoinSessionUseCase( sessions.hasSession(userId, lastActiveTime) -> Result.AlreadyInSession else -> { sessions.addUser(timerId, userId, time.provide()) - sessions.sendEvent(timerId, TimerEvent.UserJoined(userId)) + fsm.sendEvent(timerId, TimerEvent.UserJoined(userId)) Result.Success } diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/LeaveSessionUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/LeaveSessionUseCase.kt similarity index 50% rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/LeaveSessionUseCase.kt rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/LeaveSessionUseCase.kt index 0fcc0305..a8b6a2d2 100644 --- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/LeaveSessionUseCase.kt +++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/LeaveSessionUseCase.kt @@ -1,20 +1,22 @@ -package io.timemates.backend.timers.usecases.sessions +package org.timemates.backend.timers.domain.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.types.TimerEvent -import io.timemates.backend.timers.types.TimersScope -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.fsm.TimersStateMachine +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 LeaveSessionUseCase( private val sessions: TimerSessionRepository, + private val fsm: TimersStateMachine, private val timeProvider: TimeProvider, -) : UseCase { - context (AuthorizedContext) - suspend fun execute(): Result { +) { + + suspend fun execute(auth: Authorized): Result { + val userId = auth.userId val timerId = sessions.getTimerIdOfCurrentSession( userId = userId, lastActiveTime = timeProvider.provide() - 15.minutes @@ -24,7 +26,7 @@ class LeaveSessionUseCase( timerId = timerId, userId = userId, ) - sessions.sendEvent(timerId, TimerEvent.UserLeft(userId)) + fsm.sendEvent(timerId, TimerEvent.UserLeft(userId)) return Result.Success } diff --git a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/PingSessionUseCase.kt b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/PingSessionUseCase.kt similarity index 58% rename from core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/PingSessionUseCase.kt rename to features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/PingSessionUseCase.kt index 1bdfd1e8..188d4956 100644 --- a/core/src/main/kotlin/io/timemates/backend/timers/usecases/sessions/PingSessionUseCase.kt +++ b/features/timers/domain/src/main/kotlin/org/timemates/backend/timers/domain/usecases/sessions/PingSessionUseCase.kt @@ -1,19 +1,19 @@ -package io.timemates.backend.timers.usecases.sessions +package org.timemates.backend.timers.domain.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.types.TimersScope -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.TimerSessionRepository +import org.timemates.backend.types.timers.TimersScope import kotlin.time.Duration.Companion.minutes class PingSessionUseCase( private val sessions: TimerSessionRepository, private val timeProvider: TimeProvider, -) : UseCase { - context (AuthorizedContext) - suspend fun execute(): Result { +) { + + suspend fun execute(auth: Authorized): Result { + val userId = auth.userId val currentTime = timeProvider.provide() val timerId = sessions.getTimerIdOfCurrentSession( diff --git a/features/users/data/build.gradle.kts b/features/users/data/build.gradle.kts new file mode 100644 index 00000000..affb42b1 --- /dev/null +++ b/features/users/data/build.gradle.kts @@ -0,0 +1,25 @@ +plugins { + id(libs.plugins.jvm.module.convention.get().pluginId) +} + +tasks.withType { + useJUnitPlatform() +} + +dependencies { + implementation(projects.features.users.domain) + implementation(projects.foundation.exposedUtils) + implementation(projects.foundation.hashing) + + 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/users/PostgresqlUsersRepository.kt b/features/users/data/src/main/kotlin/org/timemates/backend/users/data/PostgresqlUsersRepository.kt similarity index 72% rename from data/src/main/kotlin/io/timemates/backend/data/users/PostgresqlUsersRepository.kt rename to features/users/data/src/main/kotlin/org/timemates/backend/users/data/PostgresqlUsersRepository.kt index 81c0c94b..7884ad90 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/users/PostgresqlUsersRepository.kt +++ b/features/users/data/src/main/kotlin/org/timemates/backend/users/data/PostgresqlUsersRepository.kt @@ -1,23 +1,23 @@ -package io.timemates.backend.data.users +package org.timemates.backend.users.data import com.timemates.backend.time.UnixTime -import io.timemates.backend.validation.createOrThrowInternally -import io.timemates.backend.data.users.datasource.CachedUsersDataSource -import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource -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 -import timemates.backend.hashing.repository.HashingRepository -import io.timemates.backend.users.repositories.UsersRepository as UsersRepositoryContract +import org.timemates.backend.types.users.User +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.data.datasource.CachedUsersDataSource +import org.timemates.backend.users.data.datasource.PostgresqlUsersDataSource +import org.timemates.backend.users.domain.repositories.UsersRepository +import org.timemates.backend.validation.annotations.ValidationDelicateApi +import org.timemates.backend.validation.createUnsafe class PostgresqlUsersRepository( private val postgresqlUsers: PostgresqlUsersDataSource, private val cachedUsers: CachedUsersDataSource, - private val hashingRepository: HashingRepository, private val mapper: UserEntitiesMapper, -) : UsersRepositoryContract { +) : UsersRepository { + @OptIn(ValidationDelicateApi::class) override suspend fun createUser( userEmailAddress: EmailAddress, userName: UserName, @@ -29,7 +29,7 @@ class PostgresqlUsersRepository( userName.string, shortBio?.string, creationTime.inMilliseconds - ).let { UserId.createOrThrowInternally(it) } + ).let { UserId.createUnsafe(it) } } override suspend fun isUserExists(userId: UserId): Boolean { @@ -39,9 +39,10 @@ class PostgresqlUsersRepository( return postgresqlUsers.isUserExists(userId.long) } + @OptIn(ValidationDelicateApi::class) override suspend fun getUserIdByEmail(emailAddress: EmailAddress): UserId? { return postgresqlUsers.getUserByEmail(emailAddress.string)?.id - ?.let { UserId.createOrThrowInternally(it) } + ?.let { UserId.createUnsafe(it) } } override suspend fun getUser(id: UserId): User? { diff --git a/features/users/data/src/main/kotlin/org/timemates/backend/users/data/UserEntitiesMapper.kt b/features/users/data/src/main/kotlin/org/timemates/backend/users/data/UserEntitiesMapper.kt new file mode 100644 index 00000000..06220489 --- /dev/null +++ b/features/users/data/src/main/kotlin/org/timemates/backend/users/data/UserEntitiesMapper.kt @@ -0,0 +1,60 @@ +package org.timemates.backend.users.data + +import org.timemates.backend.types.users.Avatar +import org.timemates.backend.types.users.User +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.data.datasource.CachedUsersDataSource +import org.timemates.backend.users.data.datasource.PostgresqlUsersDataSource +import org.timemates.backend.validation.annotations.ValidationDelicateApi +import org.timemates.backend.validation.createUnsafe + +@OptIn(ValidationDelicateApi::class) +class UserEntitiesMapper { + fun toDomainUser(id: Long, cachedUser: CachedUsersDataSource.User): User = with(cachedUser) { + return User( + UserId.createUnsafe(id), + UserName.createUnsafe(name), + email?.let { EmailAddress.createUnsafe(it) }, + shortBio?.let { UserDescription.createUnsafe(it) }, + avatar = avatarFileId?.let { Avatar.FileId.createUnsafe(it) } + ?: gravatarId?.let { Avatar.GravatarId.createUnsafe(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.createUnsafe(id), + UserName.createUnsafe(userName), + EmailAddress.createUnsafe(userEmail), + userShortDesc?.let { UserDescription.createUnsafe(it) }, + avatar = pUser.userAvatarFileId?.let { Avatar.FileId.createUnsafe(it) } + ?: pUser.userGravatarId?.let { Avatar.GravatarId.createUnsafe(it) } + ) + } +} \ No newline at end of file diff --git a/data/src/main/kotlin/io/timemates/backend/data/users/datasource/CachedUsersDataSource.kt b/features/users/data/src/main/kotlin/org/timemates/backend/users/data/datasource/CachedUsersDataSource.kt similarity index 92% rename from data/src/main/kotlin/io/timemates/backend/data/users/datasource/CachedUsersDataSource.kt rename to features/users/data/src/main/kotlin/org/timemates/backend/users/data/datasource/CachedUsersDataSource.kt index 47aff8c3..c06369ec 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/users/datasource/CachedUsersDataSource.kt +++ b/features/users/data/src/main/kotlin/org/timemates/backend/users/data/datasource/CachedUsersDataSource.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.data.users.datasource +package org.timemates.backend.users.data.datasource import io.github.reactivecircus.cache4k.Cache diff --git a/data/src/main/kotlin/io/timemates/backend/data/users/datasource/PostgresqlUsersDataSource.kt b/features/users/data/src/main/kotlin/org/timemates/backend/users/data/datasource/PostgresqlUsersDataSource.kt similarity index 97% rename from data/src/main/kotlin/io/timemates/backend/data/users/datasource/PostgresqlUsersDataSource.kt rename to features/users/data/src/main/kotlin/org/timemates/backend/users/data/datasource/PostgresqlUsersDataSource.kt index 50cb9999..8b800ad1 100644 --- a/data/src/main/kotlin/io/timemates/backend/data/users/datasource/PostgresqlUsersDataSource.kt +++ b/features/users/data/src/main/kotlin/org/timemates/backend/users/data/datasource/PostgresqlUsersDataSource.kt @@ -1,9 +1,9 @@ -package io.timemates.backend.data.users.datasource +package org.timemates.backend.users.data.datasource -import io.timemates.backend.exposed.suspendedTransaction import org.jetbrains.annotations.TestOnly import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.transactions.transaction +import org.timemates.backend.exposed.suspendedTransaction class PostgresqlUsersDataSource(private val database: Database) { internal object UsersTable : Table("users") { diff --git a/data/src/test/kotlin/io/timemates/backend/data/users/UserEntitiesMapperTest.kt b/features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/UserEntitiesMapperTest.kt similarity index 86% rename from data/src/test/kotlin/io/timemates/backend/data/users/UserEntitiesMapperTest.kt rename to features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/UserEntitiesMapperTest.kt index ea663aa2..7d0aa359 100644 --- a/data/src/test/kotlin/io/timemates/backend/data/users/UserEntitiesMapperTest.kt +++ b/features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/UserEntitiesMapperTest.kt @@ -1,14 +1,15 @@ -package io.timemates.backend.data.users - -import io.timemates.backend.data.users.datasource.CachedUsersDataSource -import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource -import io.timemates.backend.testing.validation.createOrAssert -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 +package org.timemates.backend.data.users + +import org.timemates.backend.foundation.validation.test.createOrAssert +import org.timemates.backend.types.users.Avatar +import org.timemates.backend.types.users.User +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.data.UserEntitiesMapper +import org.timemates.backend.users.data.datasource.CachedUsersDataSource +import org.timemates.backend.users.data.datasource.PostgresqlUsersDataSource import kotlin.test.Test import kotlin.test.assertEquals diff --git a/data/src/test/kotlin/io/timemates/backend/data/users/UsersRepositoryTest.kt b/features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/UsersRepositoryTest.kt similarity index 87% rename from data/src/test/kotlin/io/timemates/backend/data/users/UsersRepositoryTest.kt rename to features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/UsersRepositoryTest.kt index 08faaa52..efe592c1 100644 --- a/data/src/test/kotlin/io/timemates/backend/data/users/UsersRepositoryTest.kt +++ b/features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/UsersRepositoryTest.kt @@ -1,25 +1,24 @@ -package io.timemates.backend.data.users +package org.timemates.backend.users.data.test import io.mockk.* -import io.timemates.backend.data.users.datasource.CachedUsersDataSource -import io.timemates.backend.data.users.datasource.PostgresqlUsersDataSource -import io.timemates.backend.testing.validation.createOrAssert -import io.timemates.backend.users.types.User -import io.timemates.backend.users.types.value.UserDescription -import io.timemates.backend.users.types.value.UserId -import io.timemates.backend.users.types.value.UserName import kotlinx.coroutines.runBlocking -import timemates.backend.hashing.HashingRepository -import timemates.backend.hashing.repository.HashingRepository as HashingRepositoryContract +import org.timemates.backend.foundation.validation.test.createOrAssert +import org.timemates.backend.types.users.User +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.data.PostgresqlUsersRepository +import org.timemates.backend.users.data.UserEntitiesMapper +import org.timemates.backend.users.data.datasource.CachedUsersDataSource +import org.timemates.backend.users.data.datasource.PostgresqlUsersDataSource import kotlin.test.Test class UsersRepositoryTest { private val postgresqlUsers = mockk() private val cachedUsers = mockk() private val mapper = mockk(relaxed = true) - private val hashingRepository: HashingRepositoryContract = HashingRepository() private val postgresqlUsersRepository = PostgresqlUsersRepository( - postgresqlUsers, cachedUsers, hashingRepository, mapper + postgresqlUsers, cachedUsers, mapper ) @Test diff --git a/data/src/test/kotlin/io/timemates/backend/data/users/datasource/PostgresqlUsersDataSourceTest.kt b/features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/datasource/PostgresqlUsersDataSourceTest.kt similarity index 96% rename from data/src/test/kotlin/io/timemates/backend/data/users/datasource/PostgresqlUsersDataSourceTest.kt rename to features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/datasource/PostgresqlUsersDataSourceTest.kt index 9bbfc6f5..b639eeae 100644 --- a/data/src/test/kotlin/io/timemates/backend/data/users/datasource/PostgresqlUsersDataSourceTest.kt +++ b/features/users/data/src/test/kotlin/org/timemates/backend/users/data/test/datasource/PostgresqlUsersDataSourceTest.kt @@ -1,7 +1,8 @@ -package io.timemates.backend.data.users.datasource +package org.timemates.backend.data.users.datasource import kotlinx.coroutines.runBlocking import org.jetbrains.exposed.sql.Database +import org.timemates.backend.users.data.datasource.PostgresqlUsersDataSource import kotlin.test.* class PostgresqlUsersDataSourceTest { diff --git a/features/users/dependencies/build.gradle.kts b/features/users/dependencies/build.gradle.kts new file mode 100644 index 00000000..838ce686 --- /dev/null +++ b/features/users/dependencies/build.gradle.kts @@ -0,0 +1,14 @@ +plugins { + id(libs.plugins.jvm.module.convention.get().pluginId) + alias(libs.plugins.ksp) +} + +dependencies { + implementation(projects.features.users.domain) + implementation(projects.features.users.data) + + 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/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/UsersModule.kt b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/UsersModule.kt new file mode 100644 index 00000000..0d637c1a --- /dev/null +++ b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/UsersModule.kt @@ -0,0 +1,15 @@ +package org.timemates.backend.users.deps + +import org.koin.core.annotation.Module +import org.timemates.backend.users.deps.repositories.UsersRepositoriesModule +import org.timemates.backend.users.deps.usecases.EditUserUseCaseModule +import org.timemates.backend.users.deps.usecases.GetUsersUseCaseModule + +@Module( + includes = [ + UsersRepositoriesModule::class, + EditUserUseCaseModule::class, + GetUsersUseCaseModule::class, + ], +) +class UsersModule \ No newline at end of file diff --git a/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/UsersRepositoriesModule.kt b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/UsersRepositoriesModule.kt new file mode 100644 index 00000000..5f420f40 --- /dev/null +++ b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/UsersRepositoriesModule.kt @@ -0,0 +1,34 @@ +package org.timemates.backend.users.deps.repositories + +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module +import org.timemates.backend.users.data.PostgresqlUsersRepository +import org.timemates.backend.users.data.UserEntitiesMapper +import org.timemates.backend.users.data.datasource.CachedUsersDataSource +import org.timemates.backend.users.data.datasource.PostgresqlUsersDataSource +import org.timemates.backend.users.deps.repositories.cache.CacheUsersModule +import org.timemates.backend.users.deps.repositories.database.DatabaseUsersModule +import org.timemates.backend.users.deps.repositories.mappers.UsersMappersModule +import org.timemates.backend.users.domain.repositories.UsersRepository + +@Module( + includes = [ + CacheUsersModule::class, + DatabaseUsersModule::class, + UsersMappersModule::class, + ] +) +class UsersRepositoriesModule { + @Factory + fun usersRepository( + postgresqlUsersDataSource: PostgresqlUsersDataSource, + cachedUsersDataSource: CachedUsersDataSource, + usersEntitiesMapper: UserEntitiesMapper, + ): UsersRepository { + return PostgresqlUsersRepository( + postgresqlUsers = postgresqlUsersDataSource, + cachedUsers = cachedUsersDataSource, + mapper = usersEntitiesMapper, + ) + } +} \ No newline at end of file diff --git a/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/cache/CacheUsersModule.kt b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/cache/CacheUsersModule.kt new file mode 100644 index 00000000..a2f6525b --- /dev/null +++ b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/cache/CacheUsersModule.kt @@ -0,0 +1,16 @@ +package org.timemates.backend.users.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.users.data.datasource.CachedUsersDataSource + +@Module +class CacheUsersModule { + @Factory + fun cachedUsersDs( + @Named("users.cache.size") maxEntries: Long, + ): CachedUsersDataSource { + return CachedUsersDataSource(maxEntries) + } +} \ No newline at end of file diff --git a/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/database/DatabaseUsersModule.kt b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/database/DatabaseUsersModule.kt new file mode 100644 index 00000000..51dddf7a --- /dev/null +++ b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/database/DatabaseUsersModule.kt @@ -0,0 +1,14 @@ +package org.timemates.backend.users.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.users.data.datasource.PostgresqlUsersDataSource + +@Module +class DatabaseUsersModule { + @Factory + fun usersDs(database: Database): PostgresqlUsersDataSource { + return PostgresqlUsersDataSource(database) + } +} \ No newline at end of file diff --git a/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/mappers/UsersMappersModule.kt b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/mappers/UsersMappersModule.kt new file mode 100644 index 00000000..22e0647f --- /dev/null +++ b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/repositories/mappers/UsersMappersModule.kt @@ -0,0 +1,11 @@ +package org.timemates.backend.users.deps.repositories.mappers + +import org.koin.core.annotation.Module +import org.koin.core.annotation.Singleton +import org.timemates.backend.users.data.UserEntitiesMapper + +@Module +class UsersMappersModule { + @Singleton + fun userEntitiesMapper(): UserEntitiesMapper = UserEntitiesMapper() +} \ No newline at end of file diff --git a/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/usecases/EditUserUseCaseModule.kt b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/usecases/EditUserUseCaseModule.kt new file mode 100644 index 00000000..2160c1c5 --- /dev/null +++ b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/usecases/EditUserUseCaseModule.kt @@ -0,0 +1,14 @@ +package org.timemates.backend.users.deps.usecases + +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module +import org.timemates.backend.users.domain.repositories.UsersRepository +import org.timemates.backend.users.domain.usecases.EditUserUseCase + +@Module +class EditUserUseCaseModule { + @Factory + fun useCase(usersRepository: UsersRepository): EditUserUseCase { + return EditUserUseCase(usersRepository) + } +} \ No newline at end of file diff --git a/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/usecases/GetUsersUseCaseModule.kt b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/usecases/GetUsersUseCaseModule.kt new file mode 100644 index 00000000..5414252c --- /dev/null +++ b/features/users/dependencies/src/main/kotlin/org/timemates/backend/users/deps/usecases/GetUsersUseCaseModule.kt @@ -0,0 +1,14 @@ +package org.timemates.backend.users.deps.usecases + +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module +import org.timemates.backend.users.domain.repositories.UsersRepository +import org.timemates.backend.users.domain.usecases.GetUsersUseCase + +@Module +class GetUsersUseCaseModule { + @Factory + fun useCase(usersRepository: UsersRepository): GetUsersUseCase { + return GetUsersUseCase(usersRepository) + } +} \ No newline at end of file diff --git a/features/users/domain/build.gradle.kts b/features/users/domain/build.gradle.kts new file mode 100644 index 00000000..54cd871b --- /dev/null +++ b/features/users/domain/build.gradle.kts @@ -0,0 +1,15 @@ +plugins { + id(libs.plugins.jvm.module.convention.get().pluginId) +} + +dependencies { + implementation(projects.foundation.time) + implementation(projects.foundation.pageToken) + + api(projects.core.types) + implementation(projects.core.types.authIntegration) +} + +tasks.withType { + useJUnitPlatform() +} \ No newline at end of file diff --git a/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/UsersScope.kt b/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/UsersScope.kt new file mode 100644 index 00000000..b1fe07de --- /dev/null +++ b/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/UsersScope.kt @@ -0,0 +1,11 @@ +package org.timemates.backend.users.domain + +import org.timemates.backend.foundation.authorization.Scope + +sealed class UsersScope : Scope { + open class Read : UsersScope() { + companion object : Read() + } + + data object Write : Read() +} \ No newline at end of file diff --git a/core/src/main/kotlin/io/timemates/backend/users/repositories/UsersRepository.kt b/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/repositories/UsersRepository.kt similarity index 63% rename from core/src/main/kotlin/io/timemates/backend/users/repositories/UsersRepository.kt rename to features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/repositories/UsersRepository.kt index 790d919b..1d172d77 100644 --- a/core/src/main/kotlin/io/timemates/backend/users/repositories/UsersRepository.kt +++ b/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/repositories/UsersRepository.kt @@ -1,14 +1,13 @@ -package io.timemates.backend.users.repositories +package org.timemates.backend.users.domain.repositories import com.timemates.backend.time.UnixTime -import io.timemates.backend.common.markers.Repository -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 +import org.timemates.backend.types.users.User +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 : Repository { +interface UsersRepository { suspend fun createUser( userEmailAddress: EmailAddress, userName: UserName, diff --git a/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/usecases/EditUserUseCase.kt b/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/usecases/EditUserUseCase.kt new file mode 100644 index 00000000..559ddae0 --- /dev/null +++ b/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/usecases/EditUserUseCase.kt @@ -0,0 +1,24 @@ +package org.timemates.backend.users.domain.usecases + +import org.timemates.backend.core.types.integration.auth.userId +import org.timemates.backend.foundation.authorization.Authorized +import org.timemates.backend.types.users.User +import org.timemates.backend.users.domain.UsersScope +import org.timemates.backend.users.domain.repositories.UsersRepository + +class EditUserUseCase( + private val usersRepository: UsersRepository, +) { + suspend fun execute( + auth: Authorized, + patch: User.Patch, + ): Result { + return if (usersRepository.edit(auth.userId, patch)) Result.Success else Result.Failed + } + + sealed interface Result { + data object Success : Result + + data object Failed : Result + } +} \ No newline at end of file diff --git a/core/src/main/kotlin/io/timemates/backend/users/usecases/GetUsersUseCase.kt b/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/usecases/GetUsersUseCase.kt similarity index 55% rename from core/src/main/kotlin/io/timemates/backend/users/usecases/GetUsersUseCase.kt rename to features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/usecases/GetUsersUseCase.kt index da66c58d..50d92f51 100644 --- a/core/src/main/kotlin/io/timemates/backend/users/usecases/GetUsersUseCase.kt +++ b/features/users/domain/src/main/kotlin/org/timemates/backend/users/domain/usecases/GetUsersUseCase.kt @@ -1,13 +1,12 @@ -package io.timemates.backend.users.usecases +package org.timemates.backend.users.domain.usecases -import io.timemates.backend.common.markers.UseCase -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.types.users.User +import org.timemates.backend.types.users.value.UserId +import org.timemates.backend.users.domain.repositories.UsersRepository class GetUsersUseCase( private val usersRepository: UsersRepository, -) : UseCase { +) { suspend fun execute(collection: List): Result { return Result.Success(usersRepository.getUsers(collection)) } diff --git a/common/README.md b/foundation/README.md similarity index 90% rename from common/README.md rename to foundation/README.md index 68f84832..241527b5 100644 --- a/common/README.md +++ b/foundation/README.md @@ -1,5 +1,6 @@ -# Common module (set of libraries) -It's module with the set of libraries that used in this project. +# Foundation module (set of libraries) + +It's module with the set of small libraries that is used in this project. - **[authorization](authorization)**: Provides an explicit way to handle authorization in use cases without boilerplate code. diff --git a/common/authorization/README.md b/foundation/authorization/README.md similarity index 90% rename from common/authorization/README.md rename to foundation/authorization/README.md index 52ca0538..915bbfc8 100644 --- a/common/authorization/README.md +++ b/foundation/authorization/README.md @@ -62,9 +62,12 @@ override suspend fun setUser(request: EditUserRequestOuterClass.EditUserRequest) } } ``` -In this example, the `EditUserUseCase` requires the `UsersScope.Write` authorization context, indicating that the use case -can only be executed by users with write access to user-related operations. The `provideAuthorizationContext` -[(source)](/../../infrastructure/grpc-api/src/main/kotlin/io/timemates/backend/services/authorization/context/provideAuthorizationContext.kt) function ensures that the authorization context is available when executing the use case. + +In this example, the `EditUserUseCase` requires the `UsersScope.Write` authorization context, indicating that the use +case +can only be executed by users with write access to user-related operations. The `provideAuthorizationContext` +[(source)](/../../infrastructure/grpc-api/src/main/kotlin/org.timemates.backend/services/authorization/context/provideAuthorizationContext.kt) +function ensures that the authorization context is available when executing the use case. By using the 'authorization' library, you can have clear and explicit authorization requirements in your use cases, enhancing the security and control of your application. \ No newline at end of file diff --git a/common/authorization/build.gradle.kts b/foundation/authorization/build.gradle.kts similarity index 100% rename from common/authorization/build.gradle.kts rename to foundation/authorization/build.gradle.kts diff --git a/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/AuthDelicateApi.kt b/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/AuthDelicateApi.kt new file mode 100644 index 00000000..ac08eee6 --- /dev/null +++ b/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/AuthDelicateApi.kt @@ -0,0 +1,4 @@ +package org.timemates.backend.foundation.authorization + +@RequiresOptIn(message = "This API shouldn't be used directly, but through providers") +public annotation class AuthDelicateApi \ No newline at end of file diff --git a/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/Authorized.kt b/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/Authorized.kt new file mode 100644 index 00000000..54ef8285 --- /dev/null +++ b/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/Authorized.kt @@ -0,0 +1,8 @@ +package org.timemates.backend.foundation.authorization + +import org.timemates.backend.foundation.authorization.types.AuthorizedId + + +public class Authorized<@Suppress("unused") T : Scope> @AuthDelicateApi constructor( + public val id: AuthorizedId, +) diff --git a/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/Scope.kt b/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/Scope.kt similarity index 75% rename from common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/Scope.kt rename to foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/Scope.kt index f85f978c..ef263352 100644 --- a/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/Scope.kt +++ b/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/Scope.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.features.authorization +package org.timemates.backend.foundation.authorization /** * Represents authorization scope. diff --git a/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/types/AuthorizedId.kt b/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/types/AuthorizedId.kt similarity index 51% rename from common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/types/AuthorizedId.kt rename to foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/types/AuthorizedId.kt index e7dc0466..0cff3c9f 100644 --- a/common/authorization/src/main/kotlin/io/timemates/backend/features/authorization/types/AuthorizedId.kt +++ b/foundation/authorization/src/main/kotlin/org/timemates/backend/foundation/authorization/types/AuthorizedId.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.features.authorization.types +package org.timemates.backend.foundation.authorization.types @JvmInline public value class AuthorizedId(public val long: Long) \ No newline at end of file diff --git a/common/cli-arguments/build.gradle.kts b/foundation/cli-arguments/build.gradle.kts similarity index 100% rename from common/cli-arguments/build.gradle.kts rename to foundation/cli-arguments/build.gradle.kts diff --git a/common/cli-arguments/src/main/kotlin/io/timemates/backend/cli/Arguments.kt b/foundation/cli-arguments/src/main/kotlin/org/timemates/backend/cli/Arguments.kt similarity index 96% rename from common/cli-arguments/src/main/kotlin/io/timemates/backend/cli/Arguments.kt rename to foundation/cli-arguments/src/main/kotlin/org/timemates/backend/cli/Arguments.kt index 458ad2c3..2854f910 100644 --- a/common/cli-arguments/src/main/kotlin/io/timemates/backend/cli/Arguments.kt +++ b/foundation/cli-arguments/src/main/kotlin/org/timemates/backend/cli/Arguments.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.cli +package org.timemates.backend.cli @JvmInline value class Arguments(private val array: Array) { diff --git a/common/coroutines-utils/build.gradle.kts b/foundation/coroutines-utils/build.gradle.kts similarity index 100% rename from common/coroutines-utils/build.gradle.kts rename to foundation/coroutines-utils/build.gradle.kts diff --git a/common/coroutines-utils/src/main/kotlin/io/timemates/backend/coroutines/FlowExt.kt b/foundation/coroutines-utils/src/main/kotlin/org/timemates/backend/coroutines/FlowExt.kt similarity index 93% rename from common/coroutines-utils/src/main/kotlin/io/timemates/backend/coroutines/FlowExt.kt rename to foundation/coroutines-utils/src/main/kotlin/org/timemates/backend/coroutines/FlowExt.kt index d7a8e316..473c50ae 100644 --- a/common/coroutines-utils/src/main/kotlin/io/timemates/backend/coroutines/FlowExt.kt +++ b/foundation/coroutines-utils/src/main/kotlin/org/timemates/backend/coroutines/FlowExt.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.coroutines +package org.timemates.backend.coroutines import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow diff --git a/common/exposed-utils/build.gradle.kts b/foundation/exposed-utils/build.gradle.kts similarity index 100% rename from common/exposed-utils/build.gradle.kts rename to foundation/exposed-utils/build.gradle.kts diff --git a/common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/SuspendedTransaction.kt b/foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/SuspendedTransaction.kt similarity index 96% rename from common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/SuspendedTransaction.kt rename to foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/SuspendedTransaction.kt index fbda1c45..a4ca5146 100644 --- a/common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/SuspendedTransaction.kt +++ b/foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/SuspendedTransaction.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.exposed +package org.timemates.backend.exposed import kotlinx.coroutines.Dispatchers import org.jetbrains.exposed.sql.Database diff --git a/common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/emptyAsDefault.kt b/foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/emptyAsDefault.kt similarity index 86% rename from common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/emptyAsDefault.kt rename to foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/emptyAsDefault.kt index dbbfc9aa..a3a3eb2c 100644 --- a/common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/emptyAsDefault.kt +++ b/foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/emptyAsDefault.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.exposed +package org.timemates.backend.exposed import org.jetbrains.exposed.sql.Column import org.jetbrains.exposed.sql.Table.Dual.default diff --git a/common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/statements.kt b/foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/statements.kt similarity index 96% rename from common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/statements.kt rename to foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/statements.kt index 2a7d0359..7abfd3d6 100644 --- a/common/exposed-utils/src/main/kotlin/io/timemates/backend/exposed/statements.kt +++ b/foundation/exposed-utils/src/main/kotlin/org/timemates/backend/exposed/statements.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.exposed +package org.timemates.backend.exposed import org.jetbrains.exposed.sql.* import org.jetbrains.exposed.sql.statements.UpdateBuilder diff --git a/common/hashing/build.gradle.kts b/foundation/hashing/build.gradle.kts similarity index 100% rename from common/hashing/build.gradle.kts rename to foundation/hashing/build.gradle.kts diff --git a/foundation/hashing/src/main/kotlin/timemates/backend/hashing/Hash.kt b/foundation/hashing/src/main/kotlin/timemates/backend/hashing/Hash.kt new file mode 100644 index 00000000..310b7aec --- /dev/null +++ b/foundation/hashing/src/main/kotlin/timemates/backend/hashing/Hash.kt @@ -0,0 +1,4 @@ +package timemates.backend.hashing + +@JvmInline +value class Hash(val string: String) \ No newline at end of file diff --git a/common/hashing/src/main/kotlin/timemates/backend/hashing/HashingRepository.kt b/foundation/hashing/src/main/kotlin/timemates/backend/hashing/HashingRepository.kt similarity index 50% rename from common/hashing/src/main/kotlin/timemates/backend/hashing/HashingRepository.kt rename to foundation/hashing/src/main/kotlin/timemates/backend/hashing/HashingRepository.kt index 6a7b362c..f2bc1e89 100644 --- a/common/hashing/src/main/kotlin/timemates/backend/hashing/HashingRepository.kt +++ b/foundation/hashing/src/main/kotlin/timemates/backend/hashing/HashingRepository.kt @@ -2,13 +2,14 @@ package timemates.backend.hashing import java.math.BigInteger import java.security.MessageDigest -import timemates.backend.hashing.repository.HashingRepository as HashingRepositoryContract -class HashingRepository : HashingRepositoryContract { - override fun generateMD5Hash(value: String): String { +class HashingRepository { + fun md5(value: String): Hash { val md = MessageDigest.getInstance("MD5") return BigInteger( 1, md.digest(value.trim().lowercase().toByteArray()) - ).toString(16).padStart(32, '0') + ).toString(16).padStart(32, '0').let { + Hash(it) + } } } \ No newline at end of file diff --git a/common/page-token/build.gradle.kts b/foundation/page-token/build.gradle.kts similarity index 100% rename from common/page-token/build.gradle.kts rename to foundation/page-token/build.gradle.kts diff --git a/common/page-token/src/main/kotlin/io/timemates/backend/pagination/Ordering.kt b/foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/Ordering.kt similarity index 72% rename from common/page-token/src/main/kotlin/io/timemates/backend/pagination/Ordering.kt rename to foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/Ordering.kt index e7f9ead8..85d3ca6e 100644 --- a/common/page-token/src/main/kotlin/io/timemates/backend/pagination/Ordering.kt +++ b/foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/Ordering.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.pagination +package org.timemates.backend.pagination public enum class Ordering { ASCENDING, diff --git a/common/page-token/src/main/kotlin/io/timemates/backend/pagination/Page.kt b/foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/Page.kt similarity index 94% rename from common/page-token/src/main/kotlin/io/timemates/backend/pagination/Page.kt rename to foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/Page.kt index 87a96c73..fea3ac14 100644 --- a/common/page-token/src/main/kotlin/io/timemates/backend/pagination/Page.kt +++ b/foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/Page.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.pagination +package org.timemates.backend.pagination /** * A data class that represents a paginated list of items of type [T], along with a [nextPageToken]. diff --git a/common/page-token/src/main/kotlin/io/timemates/backend/pagination/PageToken.kt b/foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/PageToken.kt similarity index 97% rename from common/page-token/src/main/kotlin/io/timemates/backend/pagination/PageToken.kt rename to foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/PageToken.kt index 0b03ee01..e9aca881 100644 --- a/common/page-token/src/main/kotlin/io/timemates/backend/pagination/PageToken.kt +++ b/foundation/page-token/src/main/kotlin/org/timemates/backend/pagination/PageToken.kt @@ -1,6 +1,6 @@ @file:OptIn(ExperimentalEncodingApi::class) -package io.timemates.backend.pagination +package org.timemates.backend.pagination import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi diff --git a/common/random/build.gradle.kts b/foundation/random/build.gradle.kts similarity index 100% rename from common/random/build.gradle.kts rename to foundation/random/build.gradle.kts diff --git a/common/random/src/main/kotlin/com/timemates/random/RandomProvider.kt b/foundation/random/src/main/kotlin/com/timemates/random/RandomProvider.kt similarity index 100% rename from common/random/src/main/kotlin/com/timemates/random/RandomProvider.kt rename to foundation/random/src/main/kotlin/com/timemates/random/RandomProvider.kt diff --git a/common/random/src/main/kotlin/com/timemates/random/SecureRandomProvider.kt b/foundation/random/src/main/kotlin/com/timemates/random/SecureRandomProvider.kt similarity index 100% rename from common/random/src/main/kotlin/com/timemates/random/SecureRandomProvider.kt rename to foundation/random/src/main/kotlin/com/timemates/random/SecureRandomProvider.kt diff --git a/common/smtp-mailer/README.md b/foundation/smtp-mailer/README.md similarity index 100% rename from common/smtp-mailer/README.md rename to foundation/smtp-mailer/README.md diff --git a/common/smtp-mailer/build.gradle.kts b/foundation/smtp-mailer/build.gradle.kts similarity index 100% rename from common/smtp-mailer/build.gradle.kts rename to foundation/smtp-mailer/build.gradle.kts diff --git a/common/smtp-mailer/src/main/kotlin/io/timemates/backend/mailer/SMTPMailer.kt b/foundation/smtp-mailer/src/main/kotlin/org/timemates/backend/mailer/SMTPMailer.kt similarity index 96% rename from common/smtp-mailer/src/main/kotlin/io/timemates/backend/mailer/SMTPMailer.kt rename to foundation/smtp-mailer/src/main/kotlin/org/timemates/backend/mailer/SMTPMailer.kt index abbfe75a..1399d521 100644 --- a/common/smtp-mailer/src/main/kotlin/io/timemates/backend/mailer/SMTPMailer.kt +++ b/foundation/smtp-mailer/src/main/kotlin/org/timemates/backend/mailer/SMTPMailer.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.mailer +package org.timemates.backend.mailer import org.simplejavamail.email.EmailBuilder import org.simplejavamail.mailer.MailerBuilder diff --git a/common/state-machine/README.md b/foundation/state-machine/README.md similarity index 100% rename from common/state-machine/README.md rename to foundation/state-machine/README.md diff --git a/foundation/state-machine/build.gradle.kts b/foundation/state-machine/build.gradle.kts new file mode 100644 index 00000000..f575cf42 --- /dev/null +++ b/foundation/state-machine/build.gradle.kts @@ -0,0 +1,17 @@ +plugins { + id(libs.plugins.jvm.module.convention.get().pluginId) +} + +dependencies { + implementation(projects.foundation.time) + implementation(projects.foundation.validation) + implementation(libs.kotlinx.coroutines) + + testImplementation(libs.kotlinx.coroutines.test) + testImplementation(libs.kotlin.test) + testImplementation(libs.mockk) +} + +kotlin { + explicitApi() +} \ No newline at end of file diff --git a/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/CoroutinesStateMachine.kt b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/CoroutinesStateMachine.kt new file mode 100644 index 00000000..ee22d940 --- /dev/null +++ b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/CoroutinesStateMachine.kt @@ -0,0 +1,102 @@ +package org.timemates.backend.fsm + +import com.timemates.backend.time.TimeProvider +import kotlinx.coroutines.* +import kotlinx.coroutines.channels.Channel +import kotlinx.coroutines.channels.consumeEach +import kotlinx.coroutines.flow.* +import java.util.concurrent.ConcurrentHashMap +import kotlin.time.Duration.Companion.minutes + + +public class CoroutinesStateMachine> internal constructor( + private val storage: StateStorage? = null, + private val timeProvider: TimeProvider, + private val controller: StateMachineController, + private val coroutineScope: CoroutineScope, +) : StateMachine { + /** + * A concurrent hash map that holds the current state of each state machine identified by a [IdType]. + */ + private val states: ConcurrentHashMap> = ConcurrentHashMap() + + /** + * A concurrent hash map that holds the shared flow of events for each state machine identified by a [IdType]. + */ + private val events: ConcurrentHashMap> = ConcurrentHashMap() + + /** + * Sends an event to the state machine identified by a [IdType]. + * + * @param key the key that identifies the state machine. + * @param event the event to be sent. + */ + override suspend fun sendEvent(key: IdType, event: EventType): Boolean { + getState(key).first() // we initialize state if it wasn't initialized before + events[key]?.send(event) ?: return false + return true + } + + /** + * Gets the state flow of the state machine identified by a [IdType]. + * + * @param id the key that identifies the state machine. + * @return a [Flow] that emits the current state of the state machine. + */ + override suspend fun getState(id: IdType): SharedFlow { + return states.getOrPut(id) { + channelFlow { + val initial = controller.initial?.invoke(id) + ?: storage?.load(id) + ?: error("Couldn't get initial state – initial and storage are not implemented") + + val currentState = MutableStateFlow(initial) + var currentTimeOutJob: Job? = null + + events[id] = Channel() + + launch { + currentState.withIndex().collectLatest { (index, update) -> + send(update) + + currentTimeOutJob?.cancel() + currentTimeOutJob = launch { + delay(timeProvider.provide() - update.publishTime + update.alive) + + if (update == currentState.value) + controller[update].onTimeout?.invoke(id, update) + ?.let { currentState.value = it } + // by convention, if onTimeout of the state is not provided, and + // it reaches timeout – it's removed from the storage and channelFlow + // is closed + ?: run { + storage?.remove(id) + close() + } + } + + if (index > 0) + storage?.save(id, update) + } + } + + launch { + events[id]?.consumeEach { event -> + currentState.update { state -> + controller[state].onEvent?.invoke(id, state, event) ?: state + } + } + } + + invokeOnClose { + currentTimeOutJob?.cancel() + states.remove(id) + events.compute(id) { id, channel -> + channel?.close() + null + } + } + }.shareIn(coroutineScope, started = SharingStarted.WhileSubscribed(5.minutes), 1) + } + } +} diff --git a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/EmptyState.kt b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/EmptyState.kt similarity index 69% rename from common/state-machine/src/main/kotlin/io/timemates/backend/fsm/EmptyState.kt rename to foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/EmptyState.kt index 0bca3e50..64c1a803 100644 --- a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/EmptyState.kt +++ b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/EmptyState.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.fsm +package org.timemates.backend.fsm import com.timemates.backend.time.UnixTime import kotlin.time.Duration @@ -7,7 +7,9 @@ import kotlin.time.Duration * An implementation of the [State] class that represents an empty state with no input and no output. * This state has an infinite lifetime, and ignores all input events. */ -public object EmptyState : State() { +public object EmptyState : State, State.Key { override val alive: Duration = Duration.INFINITE override val publishTime: UnixTime = UnixTime.ZERO + override val key: State.Key<*> + get() = this } \ No newline at end of file diff --git a/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/FSMDsl.kt b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/FSMDsl.kt new file mode 100644 index 00000000..9c249a6a --- /dev/null +++ b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/FSMDsl.kt @@ -0,0 +1,4 @@ +package org.timemates.backend.fsm + +@DslMarker +public annotation class FSMDsl \ No newline at end of file diff --git a/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/State.kt b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/State.kt new file mode 100644 index 00000000..c7c481fa --- /dev/null +++ b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/State.kt @@ -0,0 +1,55 @@ +package org.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 interface State { + + /** + * The duration that this state should remain alive. + */ + public val alive: Duration + + /** + * When state was present. + */ + public val publishTime: UnixTime + + /** + * The identifier of the state. + * + * @see StateMachineController + */ + public val key: Key<*> + + public interface Key> +// +// /** +// * 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/state-machine/src/main/kotlin/io/timemates/backend/fsm/StateMachine.kt b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateMachine.kt similarity index 52% rename from common/state-machine/src/main/kotlin/io/timemates/backend/fsm/StateMachine.kt rename to foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateMachine.kt index 587d31e2..ebbcb40d 100644 --- a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/StateMachine.kt +++ b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateMachine.kt @@ -1,37 +1,30 @@ -package io.timemates.backend.fsm +package org.timemates.backend.fsm import kotlinx.coroutines.coroutineScope -import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.first /** * An interface representing a state machine. - * @param KeyType the type of the keys used to identify individual state machines. + * @param IdType the type of the keys used to identify individual state machines. */ -public interface StateMachine> { +public interface StateMachine> { /** - * Sets the state of the specified state machine. - * @param key the key identifying the state machine. - * @param state the new state for the state machine. - */ - public suspend fun setState(key: KeyType, state: StateType) - - /** - * Sends an event to the state machine identified by a [KeyType]. + * Sends an event to the state machine identified by a [IdType]. * * @param key the key that identifies the state machine. * @param event the event to be sent. * * @return [Boolean] true if the event was sent, false otherwise. */ - public suspend fun sendEvent(key: KeyType, event: EventType): Boolean + public suspend fun sendEvent(key: IdType, event: EventType): Boolean /** * Returns a flow of the states for the specified state machine. - * @param key the key identifying the state machine. + * @param id the identifier of the state machine. * @return a flow of the states for the state machine. */ - public suspend fun getState(key: KeyType): Flow? + public suspend fun getState(id: IdType): SharedFlow } /** @@ -39,8 +32,8 @@ public interface StateMachine> StateMachine.getCurrentState( key: KT, -): ST? { - return getState(key)?.first() +): ST { + return getState(key).first() } public suspend fun > StateMachine.getCurrentState( diff --git a/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateMachineBuilder.kt b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateMachineBuilder.kt new file mode 100644 index 00000000..74203633 --- /dev/null +++ b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateMachineBuilder.kt @@ -0,0 +1,88 @@ +package org.timemates.backend.fsm + +import com.timemates.backend.time.TimeProvider +import kotlinx.coroutines.CoroutineScope + +public fun > stateMachineController( + block: StateMachineBuilder.() -> Unit, +): StateMachineController { + return StateMachineBuilder().apply(block).build() +} + +@FSMDsl +public class StateMachineBuilder> { + private val states = mutableMapOf, StateBuilder>() + private var initial: (suspend (IdType) -> StateType)? = null + + public fun initial(block: suspend (IdType) -> StateType) { + initial = block + } + + public fun state(key: State.Key, block: StateBuilder.() -> Unit) { + @Suppress("UNCHECKED_CAST") + states += key to (StateBuilder().apply(block) + as StateBuilder) + } + + public fun state( + vararg keys: State.Key, + block: StateBuilder.() -> Unit, + ) { + keys.forEach { key -> + state(key = key, block) + } + } + + public class StateBuilder, HStateType : State> { + internal var _onEnter: (suspend (IdType, HStateType) -> BStateType)? = null + internal var _onTimeout: (suspend (IdType, HStateType) -> BStateType?)? = null + internal var _onEvent: (suspend (IdType, HStateType, EventType) -> BStateType)? = null + + public fun onEnter(block: suspend (IdType, HStateType) -> BStateType) { + _onEnter = block + } + + public fun onTimeout(block: suspend (IdType, HStateType) -> BStateType?) { + _onTimeout = block + } + + public fun onEvent(block: suspend (IdType, HStateType, EventType) -> BStateType) { + _onEvent = block + } + } + + public fun build(): StateMachineController { + return StateMachineController( + states.mapValues { (_, builder) -> + StateMachineController.StateController( + builder._onEnter, + builder._onTimeout, + builder._onEvent, + ) + }, + initial, + ) + } +} + +public data class StateMachineController>( + val states: Map, StateController>, + val initial: (suspend (IdType) -> StateType)?, +) { + public operator fun get(instance: T): StateController { + @Suppress("UNCHECKED_CAST") + return states[instance.key] as StateController + } + + public data class StateController>( + val onEnter: (suspend (IdType, StateType) -> StateType)? = null, + val onTimeout: (suspend (IdType, StateType) -> StateType?)? = null, + val onEvent: (suspend (IdType, StateType, EventType) -> StateType)? = null, + ) +} + +public fun > StateMachineController.toStateMachine( + storage: StateStorage, + timeProvider: TimeProvider, + coroutineScope: CoroutineScope, +): StateMachine = CoroutinesStateMachine(storage, timeProvider, this, coroutineScope) diff --git a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/StateStorage.kt b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateStorage.kt similarity index 75% rename from common/state-machine/src/main/kotlin/io/timemates/backend/fsm/StateStorage.kt rename to foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateStorage.kt index c1364d6d..9ef5ce04 100644 --- a/common/state-machine/src/main/kotlin/io/timemates/backend/fsm/StateStorage.kt +++ b/foundation/state-machine/src/main/kotlin/org/timemates/backend/fsm/StateStorage.kt @@ -1,13 +1,13 @@ -package io.timemates.backend.fsm +package org.timemates.backend.fsm /** * An interface for a state storage that can save and load FSM states. * - * @param Key the type of key used to identify the FSM state + * @param Id the type of key used to identify the FSM state * @param FsmState the type of FSM state that will be saved and loaded * @param Event the type of events that the FSM state can handle */ -public interface StateStorage, Event> { +public interface StateStorage, Event> { /** * Saves the given FSM state to the storage. @@ -16,7 +16,7 @@ public interface StateStorage, Event> { * @param state the FSM state to save * @throws Exception if an error occurs while saving the state */ - public suspend fun save(key: Key, state: FsmState) + public suspend fun save(key: Id, state: FsmState) /** * Removes the state with the given key from the storage. @@ -24,7 +24,7 @@ public interface StateStorage, Event> { * * @return [Boolean] whether the state was removed */ - public suspend fun remove(key: Key): Boolean + public suspend fun remove(key: Id): Boolean /** * Loads the FSM state with the given key from the storage. @@ -33,5 +33,5 @@ public interface StateStorage, Event> { * @return the loaded FSM state, or null if the state with the given key does not exist * @throws Exception if an error occurs while loading the state */ - public suspend fun load(key: Key): FsmState? + public suspend fun load(key: Id): FsmState? } \ No newline at end of file diff --git a/foundation/state-machine/src/test/kotlin/org/timemates/backend/fsm/CoroutineStateMachineTest.kt b/foundation/state-machine/src/test/kotlin/org/timemates/backend/fsm/CoroutineStateMachineTest.kt new file mode 100644 index 00000000..159b981e --- /dev/null +++ b/foundation/state-machine/src/test/kotlin/org/timemates/backend/fsm/CoroutineStateMachineTest.kt @@ -0,0 +1,143 @@ +package org.timemates.backend.fsm + +import com.timemates.backend.time.SystemTimeProvider +import com.timemates.backend.time.UnixTime +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.mockk +import kotlinx.coroutines.flow.first +import kotlinx.coroutines.flow.last +import kotlinx.coroutines.flow.take +import kotlinx.coroutines.test.runTest +import org.timemates.backend.validation.annotations.ValidationDelicateApi +import kotlin.test.Test +import kotlin.test.assertIs +import kotlin.time.Duration +import kotlin.time.Duration.Companion.minutes +import kotlin.time.Duration.Companion.seconds + +@OptIn(ValidationDelicateApi::class) +class CoroutineStateMachineTest { + private sealed class TestEvent { + data object ChangeToFoo : TestEvent() + data object ChangeToBar : TestEvent() + } + + private sealed class TestState : State { + data class Foo(override val publishTime: UnixTime) : TestState() { + override val alive: Duration = 10.seconds + override val key: State.Key<*> get() = Key + + companion object Key : State.Key + } + + data class Bar(override val publishTime: UnixTime) : TestState() { + override val alive: Duration = 10.seconds + override val key: State.Key<*> get() = Key + + companion object Key : State.Key + } + } + + private val storage = mockk>(relaxed = true) + private val timeProvider = SystemTimeProvider() + + private val stateMachineDef = stateMachineController { + initial { TestState.Foo(timeProvider.provide()) } + + state(TestState.Foo) { + onEvent { _, state, event -> + when (event) { + TestEvent.ChangeToBar -> TestState.Bar(timeProvider.provide()) + TestEvent.ChangeToFoo -> state + } + } + + onTimeout { _, _ -> + TestState.Bar(timeProvider.provide()) + } + } + + state(TestState.Bar) { + onTimeout { _, _ -> + TestState.Foo(timeProvider.provide()) + } + } + } + + @Test + fun `test transition by time`(): Unit = runTest { + // GIVEN + val fsm = stateMachineDef.toStateMachine(storage, timeProvider, backgroundScope) + + // THEN + val states = fsm.getState(0).take(2) + assertIs(states.first()) + + testScheduler.advanceUntilIdle() + testScheduler.runCurrent() + + assertIs(states.last()) + } + + @Test + fun `check mapping on event`(): Unit = runTest { + // GIVEN + val fsm = stateMachineDef.toStateMachine(storage, timeProvider, backgroundScope) + + // THEN + val states = fsm.getState(0).take(2) + fsm.sendEvent(0, TestEvent.ChangeToBar) + assertIs(states.last()) + } + + @Test + fun `check fsm init on sendEvent`(): Unit = runTest { + // GIVEN + val fsm = stateMachineDef.toStateMachine(storage, timeProvider, backgroundScope) + + // THEN + assert(fsm.sendEvent(0, TestEvent.ChangeToBar)) + assertIs(fsm.getState(0).take(2).last()) + } + + @Test + fun `check first state is not saved`(): Unit = runTest { + // GIVEN + val fsm = stateMachineDef.toStateMachine(storage, timeProvider, backgroundScope) + + // THEN + fsm.getState(0).first() + coVerify(inverse = true) { storage.save(any(), any()) } + } + + @Test + fun `check second state is saved`(): Unit = runTest { + // GIVEN + val fsm = stateMachineDef.toStateMachine(storage, timeProvider, backgroundScope) + + // THEN + fsm.sendEvent(0, TestEvent.ChangeToBar) + fsm.getState(0).take(2).last() + coVerify { storage.save(0, any()) } + } + + @Test + fun `check that after subscription timeout resources cleared correctly`(): Unit = runTest { + // GIVEN + val fsm = stateMachineDef.copy(initial = null) + .toStateMachine(storage, timeProvider, backgroundScope) + + // WHEN + coEvery { storage.load(any()) } returns TestState.Foo(timeProvider.provide()) + + // THEN + fsm.getState(0).first() + testScheduler.advanceTimeBy(5.minutes.inWholeMilliseconds) + testScheduler.runCurrent() + + fsm.getState(0).first() + // we assume that if storage loads for second time, everything was cleared + coVerify(exactly = 2) { storage.load(any()) } + } +} \ No newline at end of file diff --git a/common/time/build.gradle.kts b/foundation/time/build.gradle.kts similarity index 79% rename from common/time/build.gradle.kts rename to foundation/time/build.gradle.kts index 3bdfb590..489e0fc1 100644 --- a/common/time/build.gradle.kts +++ b/foundation/time/build.gradle.kts @@ -9,5 +9,5 @@ kotlin { } dependencies { - implementation(projects.common.validation) + implementation(projects.foundation.validation) } \ No newline at end of file diff --git a/common/time/src/main/kotlin/com/timemates/backend/time/SystemTimeProvider.kt b/foundation/time/src/main/kotlin/com/timemates/backend/time/SystemTimeProvider.kt similarity index 51% rename from common/time/src/main/kotlin/com/timemates/backend/time/SystemTimeProvider.kt rename to foundation/time/src/main/kotlin/com/timemates/backend/time/SystemTimeProvider.kt index 96bb4d47..e4463508 100644 --- a/common/time/src/main/kotlin/com/timemates/backend/time/SystemTimeProvider.kt +++ b/foundation/time/src/main/kotlin/com/timemates/backend/time/SystemTimeProvider.kt @@ -1,17 +1,19 @@ package com.timemates.backend.time -import io.timemates.backend.validation.createOrThrowInternally +import org.timemates.backend.validation.annotations.ValidationDelicateApi +import org.timemates.backend.validation.createUnsafe import java.time.Clock import java.time.Instant import java.time.ZoneId public class SystemTimeProvider( - private val timeZone: ZoneId = ZoneId.systemDefault(), + timeZone: ZoneId = ZoneId.systemDefault(), ) : TimeProvider { private val clock = Clock.system(timeZone) + @OptIn(ValidationDelicateApi::class) override fun provide(): UnixTime { val instant = Instant.now(clock) - return UnixTime.createOrThrowInternally(instant.toEpochMilli()) + return UnixTime.createUnsafe(instant.toEpochMilli()) } } \ No newline at end of file diff --git a/common/time/src/main/kotlin/com/timemates/backend/time/TimeProvider.kt b/foundation/time/src/main/kotlin/com/timemates/backend/time/TimeProvider.kt similarity index 51% rename from common/time/src/main/kotlin/com/timemates/backend/time/TimeProvider.kt rename to foundation/time/src/main/kotlin/com/timemates/backend/time/TimeProvider.kt index 4732bb1b..bbfba488 100644 --- a/common/time/src/main/kotlin/com/timemates/backend/time/TimeProvider.kt +++ b/foundation/time/src/main/kotlin/com/timemates/backend/time/TimeProvider.kt @@ -1,8 +1,6 @@ package com.timemates.backend.time -import io.timemates.backend.validation.markers.InternalThrowAbility - -public interface TimeProvider : InternalThrowAbility { +public interface TimeProvider { /** * Provides current time in Unix format. */ diff --git a/common/time/src/main/kotlin/com/timemates/backend/time/UnixTime.kt b/foundation/time/src/main/kotlin/com/timemates/backend/time/UnixTime.kt similarity index 71% rename from common/time/src/main/kotlin/com/timemates/backend/time/UnixTime.kt rename to foundation/time/src/main/kotlin/com/timemates/backend/time/UnixTime.kt index 49cb388e..8233356d 100644 --- a/common/time/src/main/kotlin/com/timemates/backend/time/UnixTime.kt +++ b/foundation/time/src/main/kotlin/com/timemates/backend/time/UnixTime.kt @@ -1,9 +1,8 @@ package com.timemates.backend.time -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 org.timemates.backend.validation.CreationFailure +import org.timemates.backend.validation.SafeConstructor +import org.timemates.backend.validation.reflection.wrapperTypeName import kotlin.time.Duration import kotlin.time.Duration.Companion.milliseconds @@ -54,21 +53,17 @@ public value class UnixTime private constructor(private val long: Long) { public operator fun compareTo(other: UnixTime): Int = long.compareTo(other.long) - public companion object : SafeConstructor() { + public companion object : SafeConstructor { override val displayName: String by wrapperTypeName() public val ZERO: UnixTime = UnixTime(0) public val INFINITE: UnixTime = UnixTime(Long.MAX_VALUE) - /** - * Instantiates the instance of [UnixTime]. - * Negative [value] is now allowed and will fail. - */ - context(ValidationFailureHandler) - override fun create(value: Long): UnixTime { + + override fun create(value: Long): Result { return when { - value < 0 -> onFail(FailureMessage.ofNegative()) - else -> UnixTime(long = value) + value < 0 -> Result.failure(CreationFailure.ofMin(0)) + else -> Result.success(UnixTime(long = value)) } } } diff --git a/common/validation/README.md b/foundation/validation/README.md similarity index 100% rename from common/validation/README.md rename to foundation/validation/README.md diff --git a/common/validation/build.gradle.kts b/foundation/validation/build.gradle.kts similarity index 100% rename from common/validation/build.gradle.kts rename to foundation/validation/build.gradle.kts diff --git a/foundation/validation/src/main/kotlin/org/timemates/backend/validation/CreationFailure.kt b/foundation/validation/src/main/kotlin/org/timemates/backend/validation/CreationFailure.kt new file mode 100644 index 00000000..d7f4cfbe --- /dev/null +++ b/foundation/validation/src/main/kotlin/org/timemates/backend/validation/CreationFailure.kt @@ -0,0 +1,87 @@ +package org.timemates.backend.validation + +/** + * Represents a failure that occurs during the creation of an object. + * + * This class extends the `TimeMatesException` class and provides additional functionality + * specific to creation failures. + * + * @property message The error message associated with the creation failure. + */ +public sealed class CreationFailure(message: String) : Exception(message) { + /** + * Represents a creation failure due to a size range constraint. + */ + public data class SizeRangeFailure(public val range: IntRange) : CreationFailure("Constraint failure: size must be in range of $range") + + /** + * Represents a creation failure due to an exact size constraint. + */ + public data class SizeExactFailure(public val size: Int) : CreationFailure("Constraint failure: size must be exactly $size") + + /** + * Represents a creation failure due to a minimum value constraint. + */ + public data class MinValueFailure(public val size: Int) : CreationFailure("Constraint failure: minimal value is $size") + + /** + * Represents a creation failure due to a blank value constraint. + */ + public class BlankValueFailure : CreationFailure("Constraint failure: provided value is empty") + + /** + * Represents a creation failure due to a pattern constraint. + */ + public data class PatternFailure(public val regex: Regex) : CreationFailure("Constraint failure: input should match $regex") + + public companion object { + /** + * Creates a [SizeRangeFailure] object with a size constraint failure message. + * + * @param size The size range constraint for the creation failure. + * @return The [SizeRangeFailure] object with the specified size constraint failure message. + */ + public fun ofSizeRange(size: IntRange): CreationFailure { + return SizeRangeFailure(size) + } + + /** + * Creates a [SizeExactFailure] with a constraint failure message based on the provided size. + * + * @param size The expected size that caused the constraint failure. + * @return A [SizeExactFailure] object with the constraint failure message. + */ + public fun ofSizeExact(size: Int): CreationFailure { + return SizeExactFailure(size) + } + + /** + * Creates a [MinValueFailure] with a constraint failure message for a minimum value. + * + * @param size The minimal value that caused the constraint failure. + * @return A [MinValueFailure] object with the constraint failure message. + */ + public fun ofMin(size: Int): CreationFailure { + return MinValueFailure(size) + } + + /** + * Creates a [BlankValueFailure] with a constraint failure message for a blank value. + * + * @return A [BlankValueFailure] object with the constraint failure message. + */ + public fun ofBlank(): CreationFailure { + return BlankValueFailure() + } + + /** + * Creates a [PatternFailure] object with a pattern constraint failure message. + * + * @param regex The regular expression pattern constraint for the creation failure. + * @return The [PatternFailure] object with the specified pattern constraint failure message. + */ + public fun ofPattern(regex: Regex): CreationFailure { + return PatternFailure(regex) + } + } +} \ No newline at end of file diff --git a/foundation/validation/src/main/kotlin/org/timemates/backend/validation/SafeConstructor.kt b/foundation/validation/src/main/kotlin/org/timemates/backend/validation/SafeConstructor.kt new file mode 100644 index 00000000..fe016a82 --- /dev/null +++ b/foundation/validation/src/main/kotlin/org/timemates/backend/validation/SafeConstructor.kt @@ -0,0 +1,48 @@ +package org.timemates.backend.validation + +import org.timemates.backend.validation.annotations.ValidationDelicateApi + +/** + * 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. + * + * **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 interface SafeConstructor { + /** + * Name of the class what is validated. Used to display for API + * responses. + */ + public val displayName: String + + + /** + * Instantiates the entity of given type [Type]. + * + * **Shouldn't throw anything, but instantiate object of type [Type] if possible** + */ + public fun create( + value: WrappedType, + ): Result +} + +public inline fun SafeConstructor.createOr( + value: W, + otherwise: (Throwable) -> T, +): T { + return create(value).getOrElse(otherwise) +} + + +@ValidationDelicateApi +@Throws(CreationFailure::class) +public fun SafeConstructor.createUnsafe(value: W): T { + return create(value).getOrThrow() +} \ No newline at end of file diff --git a/common/validation/src/main/kotlin/io/timemates/backend/validation/annotations/ValidationDelicateApi.kt b/foundation/validation/src/main/kotlin/org/timemates/backend/validation/annotations/ValidationDelicateApi.kt similarity index 79% rename from common/validation/src/main/kotlin/io/timemates/backend/validation/annotations/ValidationDelicateApi.kt rename to foundation/validation/src/main/kotlin/org/timemates/backend/validation/annotations/ValidationDelicateApi.kt index 9a3f3513..0060759d 100644 --- a/common/validation/src/main/kotlin/io/timemates/backend/validation/annotations/ValidationDelicateApi.kt +++ b/foundation/validation/src/main/kotlin/org/timemates/backend/validation/annotations/ValidationDelicateApi.kt @@ -1,4 +1,4 @@ -package io.timemates.backend.validation.annotations +package org.timemates.backend.validation.annotations /** * The annotation that marks that API requires attention on what you do. diff --git a/foundation/validation/src/main/kotlin/org/timemates/backend/validation/reflection/wrapperTypeName.kt b/foundation/validation/src/main/kotlin/org/timemates/backend/validation/reflection/wrapperTypeName.kt new file mode 100644 index 00000000..079231ae --- /dev/null +++ b/foundation/validation/src/main/kotlin/org/timemates/backend/validation/reflection/wrapperTypeName.kt @@ -0,0 +1,7 @@ +package org.timemates.backend.validation.reflection + +import org.timemates.backend.validation.SafeConstructor + +@Suppress("UnusedReceiverParameter") +public inline fun SafeConstructor.wrapperTypeName(): Lazy = + lazy { T::class.simpleName!! } \ No newline at end of file diff --git a/foundation/validation/tests-integration/build.gradle.kts b/foundation/validation/tests-integration/build.gradle.kts new file mode 100644 index 00000000..d5180062 --- /dev/null +++ b/foundation/validation/tests-integration/build.gradle.kts @@ -0,0 +1,14 @@ +import org.jetbrains.kotlin.gradle.dsl.ExplicitApiMode + +plugins { + id(libs.plugins.jvm.module.convention.get().pluginId) +} + +kotlin { + explicitApi = ExplicitApiMode.Strict +} + +dependencies { + implementation(libs.kotlin.test) + implementation(projects.foundation.validation) +} \ No newline at end of file diff --git a/foundation/validation/tests-integration/src/main/kotlin/org/timemates/backend/foundation/validation/test/createOrAssert.kt b/foundation/validation/tests-integration/src/main/kotlin/org/timemates/backend/foundation/validation/test/createOrAssert.kt new file mode 100644 index 00000000..145419c8 --- /dev/null +++ b/foundation/validation/tests-integration/src/main/kotlin/org/timemates/backend/foundation/validation/test/createOrAssert.kt @@ -0,0 +1,11 @@ +package org.timemates.backend.foundation.validation.test + +import org.timemates.backend.validation.SafeConstructor +import org.timemates.backend.validation.createOr +import kotlin.test.asserter + +public fun SafeConstructor.createOrAssert(value: W): T { + return createOr(value) { cause -> + asserter.fail("Unable to instantiate value, provided value: `$value`.", cause) + } +} \ No newline at end of file diff --git a/gradle.properties b/gradle.properties index 2c12e232..2560e416 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1,2 @@ -kotlin.mpp.stability.nowarn=true \ No newline at end of file +kotlin.mpp.stability.nowarn=true +org.gradle.parallel=true \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index deb1d095..b17811f8 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,7 +7,7 @@ kafka = "3.3.1" jupiter = "5.4.0" exposed = "0.41.1" kmongo = "4.8.0" -mockk = "1.13.5" +mockk = "1.13.9" grpc = "1.4.0" protobuf = "3.22.0" protobuf-plugin = "0.9.2" @@ -17,9 +17,13 @@ grpc-services = "1.55.1" koin = "3.4.0" rsocket = "0.15.4" rsproto = "0.5.2" +ksp = "1.9.21-1.0.16" +koin-annotations = "1.3.0" + [libraries] kotlinx-coroutines = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" } +kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" } kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinx-serialization" } kotlinx-serialization-protobuf = { module = "org.jetbrains.kotlinx:kotlinx-serialization-protobuf", version.ref = "kotlinx-serialization" } @@ -69,7 +73,11 @@ grpc-netty = { module = "io.grpc:grpc-netty", version.ref = "grpc-netty" } grpc-services = { module = "io.grpc:grpc-services", version.ref= "grpc-services" } protobuf-kotlin = { module = "com.google.protobuf:protobuf-kotlin", version.ref = "protobuf" } protobuf-java = { module = "com.google.protobuf:protobuf-java", version.ref = "protobuf" } + koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" } +koin-annotations = { module = "io.insert-koin:koin-annotations", version.ref = "koin-annotations" } +koin-ksp-compiler = { module = "io.insert-koin:koin-ksp-compiler", version.ref = "koin-annotations" } + postgresql-driver = { module = "org.postgresql:postgresql", version.require = "42.6.0" } vanniktech-maven-publish = { module = "com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin", version.require = "0.25.3" } @@ -88,4 +96,5 @@ cashapp-sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } shadow-jar = { id = "com.github.johnrengelman.shadow", version.require = "8.1.1" } jvm-module-convention = { id = "jvm-module-convention", version.require = "SNAPSHOT" } conventions-multiplatform-library = { id = "multiplatform-library-convention", version.require = "SNAPSHOT" } -timemates-rsproto = { id = "io.timemates.rsproto", version.ref = "rsproto" } \ No newline at end of file +timemates-rsproto = { id = "io.timemates.rsproto", version.ref = "rsproto" } +ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" } \ No newline at end of file diff --git a/infrastructure/rsocket-api/build.gradle.kts b/infrastructure/rsocket-api/build.gradle.kts index 98359608..b9338e74 100644 --- a/infrastructure/rsocket-api/build.gradle.kts +++ b/infrastructure/rsocket-api/build.gradle.kts @@ -1,6 +1,7 @@ plugins { id(libs.plugins.jvm.module.convention.get().pluginId) alias(libs.plugins.kotlinx.serialization) + alias(libs.plugins.ksp) alias(libs.plugins.timemates.rsproto) } @@ -14,16 +15,23 @@ sourceSets { } dependencies { - implementation(projects.core) + implementation(projects.features.auth.domain) + implementation(projects.features.users.domain) + implementation(projects.features.timers.domain) + implementation(projects.foundation.pageToken) implementation(libs.rsocket.server) implementation(libs.rsocket.server.websockets) + implementation(libs.koin.core) + implementation(libs.koin.annotations) + ksp(libs.koin.ksp.compiler) + api(libs.timemates.rsproto.server) api(libs.timemates.rsproto.client) api(libs.timemates.rsproto.common) - implementation(projects.common.coroutinesUtils) + implementation(projects.foundation.coroutinesUtils) implementation(libs.ktor.server.core) implementation(libs.ktor.server.netty) diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/AuthorizationContext.kt b/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/AuthorizationContext.kt deleted file mode 100644 index 4fff1415..00000000 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/AuthorizationContext.kt +++ /dev/null @@ -1,38 +0,0 @@ -package io.timemates.api.rsocket.internal - -import io.timemates.api.rsocket.auth.AuthInterceptor -import io.timemates.backend.authorization.types.value.AccessHash -import io.timemates.backend.authorization.usecases.GetAuthorizationUseCase -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.authorizationProvider -import io.timemates.backend.features.authorization.types.AuthorizedId -import io.timemates.rsproto.server.RSocketService -import kotlinx.coroutines.currentCoroutineContext - -/** - * Executes the provided block of code within an authorized context. - * - * @param block The code block to be executed within the authorized context. - * @return The result of executing the code block. - */ -context(RSocketService) -internal suspend inline fun authorized( - constraint: (List) -> Boolean = { scopes -> scopes.any { it is T || it is Scope.All } }, - block: AuthorizedContext.() -> R, -): R { - val authInfo = currentCoroutineContext()[AuthInterceptor.Data] ?: unauthorized() - val accessHash = authInfo.accessHash ?: unauthorized() - - return authorizationProvider( - provider = { - authInfo.authorizationProvider.execute(AccessHash.createOrFail(accessHash)) - .let { (it as? GetAuthorizationUseCase.Result.Success)?.authorization ?: unauthorized() } - .takeIf { constraint(it.scopes) } - ?.let { Authorized(AuthorizedId(it.userId.long), scopes = it.scopes) } - }, - onFailure = { unauthorized() }, - block = block, - ) -} \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/StringExt.kt b/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/StringExt.kt deleted file mode 100644 index 300dd603..00000000 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/StringExt.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.timemates.api.rsocket.internal - -/** - * Returns null if the input string is empty or contains only whitespace characters. - * - * @return The input string if it is not empty or does not contain only whitespace characters, otherwise null. - */ -internal fun String.nullIfEmpty(): String? = takeIf { it.isNotBlank() } - -/** - * Returns the original string if it is not null, or an empty string if it is null. - * - * @return The original string if it is not null, or an empty string. - */ -@Suppress("NOTHING_TO_INLINE") -internal inline fun String?.orEmpty(): String = this ?: "" \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/RSProtoServicesModule.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/RSProtoServicesModule.kt new file mode 100644 index 00000000..e1b5d3e7 --- /dev/null +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/RSProtoServicesModule.kt @@ -0,0 +1,112 @@ +package org.timemates.api.rsocket + +import org.koin.core.annotation.Factory +import org.koin.core.annotation.Module +import org.timemates.api.rsocket.auth.AuthInterceptor +import org.timemates.api.rsocket.auth.AuthorizationService +import org.timemates.api.rsocket.timers.TimersService +import org.timemates.api.rsocket.timers.sessions.TimerSessionsService +import org.timemates.api.rsocket.users.UsersService +import org.timemates.backend.auth.domain.usecases.* +import org.timemates.backend.timers.domain.usecases.* +import org.timemates.backend.timers.domain.usecases.members.GetMembersUseCase +import org.timemates.backend.timers.domain.usecases.members.KickTimerUserUseCase +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 +import org.timemates.backend.timers.domain.usecases.sessions.* +import org.timemates.backend.users.domain.usecases.EditUserUseCase +import org.timemates.backend.users.domain.usecases.GetUsersUseCase + +@Module +class RSProtoServicesModule { + @Factory + fun authService( + authByEmailUseCase: AuthByEmailUseCase, + configureNewAccountUseCase: ConfigureNewAccountUseCase, + refreshTokenUseCase: RefreshAccessTokenUseCase, + removeAccessTokenUseCase: RemoveAccessTokenUseCase, + verifyAuthorizationUseCase: VerifyAuthorizationUseCase, + getAuthorizationsUseCase: GetAuthorizationsUseCase, + ): AuthorizationService { + return AuthorizationService( + authByEmailUseCase, + configureNewAccountUseCase, + refreshTokenUseCase, + removeAccessTokenUseCase, + verifyAuthorizationUseCase, + getAuthorizationsUseCase, + ) + } + + @Factory + fun authInterceptor( + getAuthorizationUseCase: GetAuthorizationUseCase, + ): AuthInterceptor { + return AuthInterceptor(getAuthorizationUseCase) + } + + @Factory + fun usersService( + getUsersUseCase: GetUsersUseCase, + editUserUseCase: EditUserUseCase, + ): UsersService { + return UsersService( + getUsersUseCase, + editUserUseCase, + ) + } + + @Factory + fun timersService( + createInviteUseCase: CreateInviteUseCase, + createTimerUseCase: CreateTimerUseCase, + removeTimerUseCase: RemoveTimerUseCase, + setTimerInfoUseCase: SetTimerInfoUseCase, + getInvitesUseCase: GetInvitesUseCase, + getMembersUseCase: GetMembersUseCase, + getTimersUseCase: GetTimersUseCase, + kickTimerUserUseCase: KickTimerUserUseCase, + removeInviteUseCase: RemoveInviteUseCase, + getTimerUseCase: GetTimerUseCase, + joinTimerByInvite: JoinByInviteUseCase, + ): TimersService { + return TimersService( + createInviteUseCase, + createTimerUseCase, + removeTimerUseCase, + setTimerInfoUseCase, + getInvitesUseCase, + getMembersUseCase, + getTimersUseCase, + kickTimerUserUseCase, + removeInviteUseCase, + getTimerUseCase, + joinTimerByInvite, + ) + } + + @Factory + fun timerSessionsService( + joinSessionsUseCase: JoinSessionUseCase, + leaveSessionUseCase: LeaveSessionUseCase, + startTimerUseCase: StartTimerUseCase, + stopTimerUseCase: StopTimerUseCase, + getStateUpdatesUseCase: GetStateUpdatesUseCase, + getCurrentTimerSessionUseCase: GetCurrentTimerSessionUseCase, + confirmStartUseCase: ConfirmStartUseCase, + pingSessionUseCase: PingSessionUseCase, + ): TimerSessionsService { + return TimerSessionsService( + joinSessionsUseCase, + leaveSessionUseCase, + startTimerUseCase, + stopTimerUseCase, + getStateUpdatesUseCase, + getCurrentTimerSessionUseCase, + confirmStartUseCase, + pingSessionUseCase, + ) + } +} \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/RSocketApi.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/RSocketApi.kt similarity index 77% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/RSocketApi.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/RSocketApi.kt index c3eb6288..f0af929f 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/RSocketApi.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/RSocketApi.kt @@ -1,4 +1,4 @@ -package io.timemates.api.rsocket +package org.timemates.api.rsocket import io.ktor.server.application.* import io.ktor.server.engine.* @@ -6,20 +6,24 @@ import io.ktor.server.netty.* import io.ktor.server.routing.* import io.ktor.server.websocket.* import io.rsocket.kotlin.ktor.server.RSocketSupport -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 io.timemates.rsproto.server.annotations.ExperimentalInstancesApi import io.timemates.rsproto.server.annotations.ExperimentalInterceptorsApi import io.timemates.rsproto.server.instances.protobuf import io.timemates.rsproto.server.rSocketServer import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.protobuf.ProtoBuf +import org.timemates.api.rsocket.auth.AuthInterceptor +import org.timemates.api.rsocket.auth.AuthorizationService +import org.timemates.api.rsocket.timers.TimersService +import org.timemates.api.rsocket.timers.sessions.TimerSessionsService +import org.timemates.api.rsocket.users.UsersService import java.time.Duration -@OptIn(ExperimentalInterceptorsApi::class, ExperimentalInstancesApi::class, ExperimentalSerializationApi::class) +@OptIn( + ExperimentalInterceptorsApi::class, + ExperimentalInstancesApi::class, + ExperimentalSerializationApi::class, +) fun startRSocketApi( port: Int, authorizationService: AuthorizationService, diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthInterceptor.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthInterceptor.kt similarity index 90% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthInterceptor.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthInterceptor.kt index 64d816f2..f1db4327 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthInterceptor.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthInterceptor.kt @@ -1,10 +1,10 @@ -package io.timemates.api.rsocket.auth +package org.timemates.api.rsocket.auth -import io.timemates.backend.authorization.usecases.GetAuthorizationUseCase import io.timemates.rsproto.metadata.Metadata import io.timemates.rsproto.server.annotations.ExperimentalInterceptorsApi import io.timemates.rsproto.server.interceptors.Interceptor import io.timemates.rsproto.server.interceptors.InterceptorScope +import org.timemates.backend.auth.domain.usecases.GetAuthorizationUseCase import kotlin.coroutines.CoroutineContext /** diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthMappers.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthMappers.kt similarity index 62% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthMappers.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthMappers.kt index cddecb71..ed6c472b 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthMappers.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthMappers.kt @@ -1,14 +1,14 @@ -package io.timemates.api.rsocket.auth +package org.timemates.api.rsocket.auth -import io.timemates.api.authorizations.types.Authorization -import io.timemates.api.authorizations.types.Metadata -import io.timemates.api.rsocket.internal.createOrFail -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.rsproto.server.RSocketService -import io.timemates.backend.authorization.types.Authorization as CoreAuthorization +import org.timemates.api.authorizations.types.Authorization +import org.timemates.api.authorizations.types.Metadata +import org.timemates.api.rsocket.internal.createOrFail +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.Authorization as CoreAuthorization context (RSocketService) diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthorizationService.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthorizationService.kt similarity index 69% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthorizationService.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthorizationService.kt index 1346976d..2f2dface 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/auth/AuthorizationService.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/auth/AuthorizationService.kt @@ -1,23 +1,23 @@ -package io.timemates.api.rsocket.auth +package org.timemates.api.rsocket.auth import com.google.protobuf.Empty import io.rsocket.kotlin.RSocketError -import io.timemates.api.authorizations.requests.ConfirmAuthorizationRequest -import io.timemates.api.authorizations.requests.GetAuthorizationsRequest -import io.timemates.api.authorizations.requests.RenewAuthorizationRequest -import io.timemates.api.authorizations.requests.StartAuthorizationRequest -import io.timemates.api.rsocket.internal.* -import io.timemates.api.users.requests.CreateProfileRequest -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.authorization.usecases.* -import io.timemates.backend.pagination.PageToken -import io.timemates.backend.users.types.value.EmailAddress -import io.timemates.backend.users.types.value.UserDescription -import io.timemates.backend.users.types.value.UserName -import io.timemates.api.authorizations.AuthorizationService as RSAuthorizationService +import org.timemates.api.authorizations.requests.ConfirmAuthorizationRequest +import org.timemates.api.authorizations.requests.GetAuthorizationsRequest +import org.timemates.api.authorizations.requests.RenewAuthorizationRequest +import org.timemates.api.authorizations.requests.StartAuthorizationRequest +import org.timemates.api.rsocket.internal.* +import org.timemates.api.users.requests.CreateProfileRequest +import org.timemates.backend.auth.domain.usecases.* +import org.timemates.backend.pagination.PageToken +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.types.users.value.EmailAddress +import org.timemates.backend.types.users.value.UserDescription +import org.timemates.backend.types.users.value.UserName +import org.timemates.api.authorizations.AuthorizationService as RSAuthorizationService /** * The `AuthorizationService` class provides methods to handle the authorization process. @@ -32,7 +32,7 @@ import io.timemates.api.authorizations.AuthorizationService as RSAuthorizationSe class AuthorizationService( private val authByEmailUseCase: AuthByEmailUseCase, private val configureNewAccountUseCase: ConfigureNewAccountUseCase, - private val refreshTokenUseCase: RefreshTokenUseCase, + private val refreshTokenUseCase: RefreshAccessTokenUseCase, private val removeAccessTokenUseCase: RemoveAccessTokenUseCase, private val verifyAuthorizationUseCase: VerifyAuthorizationUseCase, private val getAuthorizationsUseCase: GetAuthorizationsUseCase, @@ -51,7 +51,10 @@ class AuthorizationService( return when (result) { AuthByEmailUseCase.Result.AttemptsExceed -> attemptsExceeded() - AuthByEmailUseCase.Result.SendFailed -> internalFailure() + is AuthByEmailUseCase.Result.SendFailed -> { + result.throwable?.printStackTrace() + internalFailure() + } is AuthByEmailUseCase.Result.Success -> StartAuthorizationRequest.Result { verificationHash = result.verificationHash.string expiresAt = result.expiresAt.inMilliseconds @@ -85,6 +88,11 @@ class AuthorizationService( VerifyAuthorizationUseCase.Result.Success.NewAccount -> ConfirmAuthorizationRequest.Response { isNewAccount = true } + + is VerifyAuthorizationUseCase.Result.Failure -> { + result.throwable.printStackTrace() + internalFailure() + } } } @@ -92,8 +100,8 @@ class AuthorizationService( val result = refreshTokenUseCase.execute(RefreshHash.createOrFail(request.refreshHash)) return when (result) { - RefreshTokenUseCase.Result.InvalidAuthorization -> unauthorized() - is RefreshTokenUseCase.Result.Success -> RenewAuthorizationRequest.Response { + RefreshAccessTokenUseCase.Result.InvalidAuthorization -> unauthorized() + is RefreshAccessTokenUseCase.Result.Success -> RenewAuthorizationRequest.Response { authorization = result.auth.rs() } } @@ -111,19 +119,32 @@ class AuthorizationService( is ConfigureNewAccountUseCase.Result.Success -> CreateProfileRequest.Response { authorization = result.authorization.rs() } + + is ConfigureNewAccountUseCase.Result.Failure -> { + result.throwable.printStackTrace() + internalFailure() + } } } override suspend fun getAuthorizations( request: GetAuthorizationsRequest, - ): GetAuthorizationsRequest.Response = authorized { - val result = getAuthorizationsUseCase.execute(request.pageToken.nullIfEmpty()?.let { PageToken.accept(it) }) + ): GetAuthorizationsRequest.Response { + val result = getAuthorizationsUseCase.execute( + getAuthorization(), + request.pageToken.nullIfEmpty()?.let { PageToken.accept(it) }, + ) - when (result) { + return when (result) { is GetAuthorizationsUseCase.Result.Success -> GetAuthorizationsRequest.Response { authorizations = result.list.map { it.rs() } nextPageToken = result.nextPageToken?.forPublic().orEmpty() } + + is GetAuthorizationsUseCase.Result.AuthorizationFailure -> { + result.exception.printStackTrace() + internalFailure() + } } } diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/AnyExt.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/AnyExt.kt similarity index 93% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/AnyExt.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/AnyExt.kt index 3e1cceca..c59df5cb 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/AnyExt.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/AnyExt.kt @@ -1,4 +1,4 @@ -package io.timemates.api.rsocket.internal +package org.timemates.api.rsocket.internal import io.rsocket.kotlin.RSocketError import io.timemates.rsproto.server.RSocketService diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/ApiFailure.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/ApiFailure.kt similarity index 98% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/ApiFailure.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/ApiFailure.kt index 629daa4b..dae955a8 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/ApiFailure.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/ApiFailure.kt @@ -1,4 +1,4 @@ -package io.timemates.api.rsocket.internal +package org.timemates.api.rsocket.internal import io.ktor.http.* import io.rsocket.kotlin.RSocketError diff --git a/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/AuthorizationProvider.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/AuthorizationProvider.kt new file mode 100644 index 00000000..e40d22b7 --- /dev/null +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/AuthorizationProvider.kt @@ -0,0 +1,33 @@ +package org.timemates.api.rsocket.internal + +import io.timemates.rsproto.server.RSocketService +import kotlinx.coroutines.currentCoroutineContext +import org.timemates.api.rsocket.auth.AuthInterceptor +import org.timemates.backend.auth.domain.usecases.GetAuthorizationUseCase +import org.timemates.backend.foundation.authorization.AuthDelicateApi +import org.timemates.backend.foundation.authorization.Authorized +import org.timemates.backend.foundation.authorization.Scope +import org.timemates.backend.foundation.authorization.types.AuthorizedId +import org.timemates.backend.types.auth.value.AccessHash + +/** + * Executes the provided block of code within an authorized context. + * + * @param block The code block to be executed within the authorized context. + * @return The result of executing the code block. + */ +context(RSocketService) +internal suspend inline fun getAuthorization( + constraint: (List) -> Boolean = { scopes -> scopes.any { it is T || it is Scope.All } }, +): Authorized { + val authInfo = currentCoroutineContext()[AuthInterceptor.Data] ?: unauthorized() + val accessHash = authInfo.accessHash ?: unauthorized() + + return (authInfo.authorizationProvider.execute(AccessHash.createOrFail(accessHash)) as? GetAuthorizationUseCase.Result.Success) + ?.authorization + ?.takeIf { constraint(it.scopes) } + ?.let { + @OptIn(AuthDelicateApi::class) + Authorized(AuthorizedId(it.userId.long)) + } ?: unauthorized() +} \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/RSocketServiceExt.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/RSocketServiceExt.kt similarity index 54% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/RSocketServiceExt.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/RSocketServiceExt.kt index 592b0dba..8428a5e4 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/internal/RSocketServiceExt.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/RSocketServiceExt.kt @@ -1,23 +1,11 @@ -package io.timemates.api.rsocket.internal +package org.timemates.api.rsocket.internal import io.rsocket.kotlin.RSocketError -import io.timemates.api.rsocket.auth.AuthInterceptor -import io.timemates.backend.validation.SafeConstructor -import io.timemates.backend.validation.ValidationFailureHandler import io.timemates.rsproto.server.RSocketService import kotlinx.coroutines.currentCoroutineContext - -/** - * Used as a handler for validation inside RSocket requests. - * failures and propagates the failure to the top of the hierarchy - *. - * - * @property rSocketFailureHandler A handler for validation failures. - * @see ValidationFailureHandler - */ -private val rSocketFailureHandler = ValidationFailureHandler { message -> - throw RSocketError.Invalid(message.string) -} +import org.timemates.api.rsocket.auth.AuthInterceptor +import org.timemates.backend.validation.SafeConstructor +import org.timemates.backend.validation.createOr /** * Creates an instance of type [T] using the provided value [value] and returns it. @@ -30,12 +18,13 @@ private val rSocketFailureHandler = ValidationFailureHandler { message -> */ context(RSocketService) internal fun SafeConstructor.createOrFail(value: W): T { - return with(rSocketFailureHandler) { - create(value) + return createOr(value) { throwable -> + throw RSocketError.Invalid(throwable.message ?: "Validation failed.") } } object Request { context(RSocketService) + @JvmStatic internal suspend fun userAccessHash(): String? = currentCoroutineContext()[AuthInterceptor.Data]?.accessHash } \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/StringExt.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/StringExt.kt new file mode 100644 index 00000000..c7060a12 --- /dev/null +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/internal/StringExt.kt @@ -0,0 +1,8 @@ +package org.timemates.api.rsocket.internal + +/** + * Returns null if the input string is empty or contains only whitespace characters. + * + * @return The input string if it is not empty or does not contain only whitespace characters, otherwise null. + */ +internal fun String.nullIfEmpty(): String? = takeIf { it.isNotBlank() } \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/TimersMappers.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/TimersMappers.kt similarity index 67% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/TimersMappers.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/TimersMappers.kt index 3dbc9b67..9295e2ef 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/TimersMappers.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/TimersMappers.kt @@ -1,18 +1,17 @@ -package io.timemates.api.rsocket.timers +package org.timemates.api.rsocket.timers -import io.timemates.api.rsocket.internal.createOrFail -import io.timemates.api.timers.sessions.types.TimerState -import io.timemates.api.timers.types.Timer -import io.timemates.backend.common.types.value.Count -import io.timemates.backend.timers.fsm.* -import io.timemates.backend.timers.types.Invite -import io.timemates.backend.timers.types.TimerSettings import io.timemates.rsproto.server.RSocketService +import org.timemates.api.rsocket.internal.createOrFail +import org.timemates.api.timers.sessions.types.TimerState +import org.timemates.api.timers.types.Timer +import org.timemates.backend.types.common.value.Count +import org.timemates.backend.types.timers.Invite +import org.timemates.backend.types.timers.TimerSettings import kotlin.time.Duration.Companion.milliseconds import kotlin.time.DurationUnit -import io.timemates.api.timers.members.invites.types.Invite as RSInvite -import io.timemates.backend.timers.fsm.TimerState as CoreTimerState -import io.timemates.backend.timers.types.Timer as CoreTimer +import org.timemates.api.timers.members.invites.types.Invite as RSInvite +import org.timemates.backend.types.timers.Timer as CoreTimer +import org.timemates.backend.types.timers.TimerState as CoreTimerState context(RSocketService) internal fun Timer.Settings.core(): TimerSettings { @@ -55,17 +54,17 @@ internal fun CoreTimerState.rs(): TimerState { val endsTime = (publishTime + alive).inMilliseconds val phase = when (this) { - is ConfirmationState -> TimerState.PhaseOneOf.ConfirmationWaiting { + is CoreTimerState.ConfirmationWaiting -> TimerState.PhaseOneOf.ConfirmationWaiting { endsAt = endsTime } - is InactiveState -> TimerState.PhaseOneOf.Inactive.Default - is PauseState -> TimerState.PhaseOneOf.Paused.Default - is RestState -> TimerState.PhaseOneOf.Rest { + is CoreTimerState.Inactive -> TimerState.PhaseOneOf.Inactive.Default + is CoreTimerState.Paused -> TimerState.PhaseOneOf.Paused.Default + is CoreTimerState.Rest -> TimerState.PhaseOneOf.Rest { endsAt = endsTime } - is RunningState -> TimerState.PhaseOneOf.Running { + is CoreTimerState.Running -> TimerState.PhaseOneOf.Running { endsAt = endsTime } } diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/TimersService.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/TimersService.kt similarity index 72% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/TimersService.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/TimersService.kt index 141e17eb..1268f3c8 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/TimersService.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/TimersService.kt @@ -1,40 +1,36 @@ -package io.timemates.api.rsocket.timers +package org.timemates.api.rsocket.timers import com.google.protobuf.Empty -import io.timemates.api.rsocket.internal.* -import io.timemates.api.rsocket.users.rs -import io.timemates.api.timers.members.invites.requests.GetInvitesRequest -import io.timemates.api.timers.members.invites.requests.InviteMemberRequest -import io.timemates.api.timers.members.invites.requests.RemoveInviteRequest -import io.timemates.api.timers.members.requests.GetMembersRequest -import io.timemates.api.timers.members.requests.KickMemberRequest -import io.timemates.api.timers.requests.* -import io.timemates.api.timers.types.Timer -import io.timemates.backend.common.types.value.Count -import io.timemates.backend.pagination.PageToken -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.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 io.timemates.backend.users.types.value.UserId +import org.timemates.api.rsocket.internal.* +import org.timemates.api.rsocket.users.rs +import org.timemates.api.timers.members.invites.requests.GetInvitesRequest +import org.timemates.api.timers.members.invites.requests.InviteMemberRequest +import org.timemates.api.timers.members.invites.requests.RemoveInviteRequest +import org.timemates.api.timers.members.requests.GetMembersRequest +import org.timemates.api.timers.members.requests.KickMemberRequest +import org.timemates.api.timers.requests.* +import org.timemates.api.timers.types.Timer +import org.timemates.backend.pagination.PageToken +import org.timemates.backend.timers.domain.repositories.TimersRepository +import org.timemates.backend.timers.domain.usecases.* +import org.timemates.backend.timers.domain.usecases.members.GetMembersUseCase +import org.timemates.backend.timers.domain.usecases.members.KickTimerUserUseCase +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 +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 kotlin.time.Duration.Companion.milliseconds -import io.timemates.api.timers.TimersService as RSTimersService +import org.timemates.api.timers.TimersService as RSTimersService /** * A service class that provides various timer-related functionality. - * - * This class extends the AbstractTimersService class and implements all the required methods. - * It takes several use cases as dependencies, such as createInviteUseCase, createTimerUseCase, etc. - * These use cases provide the business logic for performing specific actions related to timers. */ class TimersService( private val createInviteUseCase: CreateInviteUseCase, @@ -57,14 +53,15 @@ class TimersService( */ override suspend fun createTimer( request: CreateTimerRequest, - ): CreateTimerRequest.Response = authorized { + ): CreateTimerRequest.Response { val result = createTimerUseCase.execute( + auth = getAuthorization(), name = TimerName.createOrFail(request.name), description = TimerDescription.createOrFail(request.description), settings = request.settings?.core() ?: TimerSettings.Default, ) - when (result) { + return when (result) { is CreateTimerUseCase.Result.Success -> CreateTimerRequest.Response { timerId = result.timerId.long } @@ -81,10 +78,10 @@ class TimersService( */ override suspend fun getTimer( request: GetTimerRequest, - ): Timer = authorized { - val result = getTimerUseCase.execute(TimerId.createOrFail(request.timerId)) + ): Timer { + val result = getTimerUseCase.execute(getAuthorization(), TimerId.createOrFail(request.timerId)) - when (result) { + return when (result) { is GetTimerUseCase.Result.Success -> result.timer.rs() GetTimerUseCase.Result.NotFound -> notFound() } @@ -98,10 +95,13 @@ class TimersService( */ override suspend fun getTimers( request: GetTimersRequest, - ): GetTimersRequest.Response = authorized { - val result = getTimersUseCase.execute(request.nextPageToken.nullIfEmpty()?.let { PageToken.accept(it) }) + ): GetTimersRequest.Response { + val result = getTimersUseCase.execute( + auth = getAuthorization(), + nextPageToken = request.nextPageToken.nullIfEmpty()?.let { PageToken.accept(it) }, + ) - when (result) { + return when (result) { is GetTimersUseCase.Result.Success -> GetTimersRequest.Response { timers = result.page.value.map { it.rs() } nextPageToken = result.page.nextPageToken?.forPublic().orEmpty() @@ -117,8 +117,9 @@ class TimersService( */ override suspend fun editTimer( request: EditTimerRequest, - ): Empty = authorized { + ): Empty { val result = setTimerInfoUseCase.execute( + auth = getAuthorization(), timerId = TimerId.createOrFail(request.timerId), patch = TimersRepository.TimerInformation.Patch( name = request.name.nullIfEmpty()?.let { TimerName.createOrFail(it) }, @@ -133,7 +134,7 @@ class TimersService( ), ) - when (result) { + return when (result) { SetTimerInfoUseCase.Result.NoAccess -> noAccess() SetTimerInfoUseCase.Result.NotFound -> notFound() SetTimerInfoUseCase.Result.Success -> Empty.Default @@ -146,13 +147,14 @@ class TimersService( * @param request The request containing the timer ID and user ID. * @return An instance of [Empty]. */ - override suspend fun kickMember(request: KickMemberRequest): Empty = authorized { + override suspend fun kickMember(request: KickMemberRequest): Empty { val result = kickTimerUserUseCase.execute( + getAuthorization(), TimerId.createOrFail(request.timerId), UserId.createOrFail(request.userId), ) - when (result) { + return when (result) { KickTimerUserUseCase.Result.NoAccess -> noAccess() KickTimerUserUseCase.Result.Success -> Empty.Default } @@ -166,13 +168,14 @@ class TimersService( */ override suspend fun getMembers( request: GetMembersRequest, - ): GetMembersRequest.Response = authorized { + ): GetMembersRequest.Response { val result = getMembersUseCase.execute( + auth = getAuthorization(), timerId = TimerId.createOrFail(request.timerId), pageToken = request.nextPageToken.nullIfEmpty()?.let { PageToken.accept(it) }, ) - when (result) { + return when (result) { GetMembersUseCase.Result.NoAccess -> noAccess() is GetMembersUseCase.Result.Success -> GetMembersRequest.Response { users = result.list.map { it.rs() } @@ -189,13 +192,14 @@ class TimersService( */ override suspend fun createInvite( request: InviteMemberRequest, - ): InviteMemberRequest.Response = authorized { + ): InviteMemberRequest.Response { val result = createInviteUseCase.execute( + auth = getAuthorization(), timerId = TimerId.createOrFail(request.timerId), limit = Count.createOrFail(request.maxJoiners), ) - when (result) { + return when (result) { CreateInviteUseCase.Result.NoAccess -> noAccess() CreateInviteUseCase.Result.TooManyCreation -> tooManyRequests() is CreateInviteUseCase.Result.Success -> InviteMemberRequest.Response { @@ -212,13 +216,14 @@ class TimersService( */ override suspend fun getInvites( request: GetInvitesRequest, - ): GetInvitesRequest.Response = authorized { + ): GetInvitesRequest.Response { val result = getInvitesUseCase.execute( + auth = getAuthorization(), timerId = TimerId.createOrFail(request.timerId), pageToken = request.nextPageToken.nullIfEmpty()?.let { PageToken.accept(it) }, ) - when (result) { + return when (result) { GetInvitesUseCase.Result.NoAccess -> noAccess() is GetInvitesUseCase.Result.Success -> GetInvitesRequest.Response { invites = result.page.value.map { it.rs() } @@ -234,13 +239,14 @@ class TimersService( */ override suspend fun removeInvite( request: RemoveInviteRequest, - ): Empty = authorized { + ): Empty { val result = removeInviteUseCase.execute( + auth = getAuthorization(), timerId = TimerId.createOrFail(request.timerId), code = InviteCode.createOrFail(request.inviteCode), ) - when (result) { + return when (result) { RemoveInviteUseCase.Result.NoAccess -> noAccess() RemoveInviteUseCase.Result.NotFound -> notFound() RemoveInviteUseCase.Result.Success -> Empty.Default @@ -249,10 +255,13 @@ class TimersService( override suspend fun joinByInvite( request: JoinTimerByInviteCodeRequest, - ): JoinTimerByInviteCodeRequest.Response = authorized { - val result = joinTimerByInvite.execute(InviteCode.createOrFail(request.inviteCode)) + ): JoinTimerByInviteCodeRequest.Response { + val result = joinTimerByInvite.execute( + getAuthorization(), + InviteCode.createOrFail(request.inviteCode), + ) - when (result) { + return when (result) { JoinByInviteUseCase.Result.NotFound -> notFound() is JoinByInviteUseCase.Result.Success -> JoinTimerByInviteCodeRequest.Response { timer = result.timer.rs() @@ -268,10 +277,13 @@ class TimersService( */ override suspend fun removeTimer( request: RemoveTimerRequest, - ): Empty = authorized { - val result = removeTimerUseCase.execute(TimerId.createOrFail(request.timerId)) + ): Empty { + val result = removeTimerUseCase.execute( + getAuthorization(), + TimerId.createOrFail(request.timerId), + ) - when (result) { + return when (result) { RemoveTimerUseCase.Result.NotFound -> notFound() RemoveTimerUseCase.Result.Success -> Empty.Default } diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/sessions/TimerSessionsService.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/sessions/TimerSessionsService.kt similarity index 54% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/sessions/TimerSessionsService.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/sessions/TimerSessionsService.kt index 82d4190f..43d20973 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/timers/sessions/TimerSessionsService.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/timers/sessions/TimerSessionsService.kt @@ -1,22 +1,22 @@ -package io.timemates.api.rsocket.timers.sessions +package org.timemates.api.rsocket.timers.sessions import com.google.protobuf.Empty import io.rsocket.kotlin.RSocketError -import io.timemates.api.rsocket.internal.* -import io.timemates.api.rsocket.timers.rs -import io.timemates.api.timers.sessions.requests.GetTimerStateRequest -import io.timemates.api.timers.sessions.requests.JoinTimerSessionRequest -import io.timemates.api.timers.sessions.requests.StartTimerRequest -import io.timemates.api.timers.sessions.requests.StopTimerRequest -import io.timemates.api.timers.sessions.types.TimerState -import io.timemates.api.timers.types.Timer -import io.timemates.backend.timers.types.value.TimerId -import io.timemates.backend.timers.usecases.StartTimerUseCase -import io.timemates.backend.timers.usecases.StopTimerUseCase -import io.timemates.backend.timers.usecases.sessions.* import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map -import io.timemates.api.timers.TimerSessionsService as RSTimerSessionsService +import org.timemates.api.rsocket.internal.* +import org.timemates.api.rsocket.timers.rs +import org.timemates.api.timers.sessions.requests.GetTimerStateRequest +import org.timemates.api.timers.sessions.requests.JoinTimerSessionRequest +import org.timemates.api.timers.sessions.requests.StartTimerRequest +import org.timemates.api.timers.sessions.requests.StopTimerRequest +import org.timemates.api.timers.sessions.types.TimerState +import org.timemates.api.timers.types.Timer +import org.timemates.backend.timers.domain.usecases.StartTimerUseCase +import org.timemates.backend.timers.domain.usecases.StopTimerUseCase +import org.timemates.backend.timers.domain.usecases.sessions.* +import org.timemates.backend.types.timers.value.TimerId +import org.timemates.api.timers.TimerSessionsService as RSTimerSessionsService class TimerSessionsService( private val joinSessionsUseCase: JoinSessionUseCase, @@ -30,20 +30,25 @@ class TimerSessionsService( ) : RSTimerSessionsService() { override suspend fun startTimer( request: StartTimerRequest, - ): Empty = authorized { - val result = startTimerUseCase.execute(TimerId.createOrFail(request.timerId)) + ): Empty { + val result = startTimerUseCase.execute( + getAuthorization(), TimerId.createOrFail(request.timerId) + ) - when (result) { + return when (result) { StartTimerUseCase.Result.NoAccess -> noAccess() StartTimerUseCase.Result.WrongState -> alreadyExists() StartTimerUseCase.Result.Success -> Empty.Default } } - override suspend fun stopTimer(request: StopTimerRequest): Empty = authorized { - val result = stopTimerUseCase.execute(TimerId.createOrFail(request.timerId)) + override suspend fun stopTimer(request: StopTimerRequest): Empty { + val result = stopTimerUseCase.execute( + auth = getAuthorization(), + timerId = TimerId.createOrFail(request.timerId), + ) - when (result) { + return when (result) { StopTimerUseCase.Result.NoAccess -> noAccess() StopTimerUseCase.Result.WrongState -> alreadyExists() StopTimerUseCase.Result.Success -> Empty.Default @@ -52,51 +57,57 @@ class TimerSessionsService( override suspend fun joinSession( request: JoinTimerSessionRequest, - ): Empty = authorized { - val result = joinSessionsUseCase.execute(TimerId.createOrFail(request.timerId)) + ): Empty { + val result = joinSessionsUseCase.execute( + getAuthorization(), + TimerId.createOrFail(request.timerId), + ) - when (result) { + return when (result) { JoinSessionUseCase.Result.AlreadyInSession -> alreadyExists() JoinSessionUseCase.Result.NotFound -> notFound() JoinSessionUseCase.Result.Success -> Empty.Default } } - override suspend fun leaveSession(request: Empty): Empty = authorized { - when (leaveSessionUseCase.execute()) { + override suspend fun leaveSession(request: Empty): Empty { + return when (leaveSessionUseCase.execute(getAuthorization())) { LeaveSessionUseCase.Result.NotFound -> notFound() LeaveSessionUseCase.Result.Success -> Empty.Default } } - override suspend fun confirmRound(request: Empty): Empty = authorized { - when (confirmStartUseCase.execute()) { + override suspend fun confirmRound(request: Empty): Empty { + return when (confirmStartUseCase.execute(getAuthorization())) { ConfirmStartUseCase.Result.NotFound -> notFound() ConfirmStartUseCase.Result.WrongState -> throw RSocketError.Invalid("Wrong State.") ConfirmStartUseCase.Result.Success -> Empty.Default } } - override suspend fun pingSession(request: Empty): Empty = authorized { - when (pingSessionUseCase.execute()) { + override suspend fun pingSession(request: Empty): Empty { + return when (pingSessionUseCase.execute(getAuthorization())) { PingSessionUseCase.Result.NoSession -> notFound() PingSessionUseCase.Result.Success -> Empty.Default } } - override suspend fun getState(request: GetTimerStateRequest): Flow = authorized { - val result = getStateUpdatesUseCase.execute(TimerId.createOrFail(request.timerId)) + override suspend fun getState(request: GetTimerStateRequest): Flow { + val result = getStateUpdatesUseCase.execute( + getAuthorization(), + TimerId.createOrFail(request.timerId), + ) - when (result) { + return when (result) { GetStateUpdatesUseCase.Result.NoAccess -> notFound() is GetStateUpdatesUseCase.Result.Success -> result.states.map { it.rs() } } } - override suspend fun getCurrentTimerSession(request: Empty): Timer = authorized { - val result = getCurrentTimerSessionUseCase.execute() + override suspend fun getCurrentTimerSession(request: Empty): Timer { + val result = getCurrentTimerSessionUseCase.execute(getAuthorization()) - when (result) { + return when (result) { GetCurrentTimerSessionUseCase.Result.NotFound -> notFound() is GetCurrentTimerSessionUseCase.Result.Success -> result.timer.rs() } diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/users/UsersMapper.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/users/UsersMapper.kt similarity index 63% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/users/UsersMapper.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/users/UsersMapper.kt index d4ee7606..141d680a 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/users/UsersMapper.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/users/UsersMapper.kt @@ -1,8 +1,8 @@ -package io.timemates.api.rsocket.users +package org.timemates.api.rsocket.users -import io.timemates.backend.users.types.Avatar -import io.timemates.backend.users.types.User -import io.timemates.api.users.types.User as RSUser +import org.timemates.backend.types.users.Avatar +import org.timemates.backend.types.users.User +import org.timemates.api.users.types.User as RSUser internal fun User.rs(): RSUser { return RSUser { diff --git a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/users/UsersService.kt b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/users/UsersService.kt similarity index 65% rename from infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/users/UsersService.kt rename to infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/users/UsersService.kt index 09e6afe9..c3984d40 100644 --- a/infrastructure/rsocket-api/src/main/kotlin/io/timemates/api/rsocket/users/UsersService.kt +++ b/infrastructure/rsocket-api/src/main/kotlin/org/timemates/api/rsocket/users/UsersService.kt @@ -1,21 +1,22 @@ -package io.timemates.api.rsocket.users +package org.timemates.api.rsocket.users import com.google.protobuf.Empty import io.rsocket.kotlin.RSocketError -import io.timemates.api.rsocket.internal.authorized -import io.timemates.api.rsocket.internal.createOrFail -import io.timemates.api.users.requests.EditEmailRequest -import io.timemates.api.users.requests.EditUserRequest -import io.timemates.api.users.requests.GetUsersRequest -import io.timemates.api.users.types.Users -import io.timemates.backend.users.types.Avatar -import io.timemates.backend.users.types.User -import io.timemates.backend.users.types.value.UserDescription -import io.timemates.backend.users.types.value.UserId -import io.timemates.backend.users.types.value.UserName -import io.timemates.backend.users.usecases.EditUserUseCase -import io.timemates.backend.users.usecases.GetUsersUseCase -import io.timemates.api.users.UsersService as AbstractUsersService +import org.timemates.api.rsocket.internal.createOrFail +import org.timemates.api.rsocket.internal.getAuthorization +import org.timemates.api.rsocket.internal.internalFailure +import org.timemates.api.users.requests.EditEmailRequest +import org.timemates.api.users.requests.EditUserRequest +import org.timemates.api.users.requests.GetUsersRequest +import org.timemates.api.users.types.Users +import org.timemates.backend.types.users.Avatar +import org.timemates.backend.types.users.User +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.usecases.EditUserUseCase +import org.timemates.backend.users.domain.usecases.GetUsersUseCase +import org.timemates.api.users.UsersService as AbstractUsersService /** * UsersService class represents a service that provides operations related to users. @@ -49,8 +50,9 @@ class UsersService( * * @see EditUserRequest */ - override suspend fun setUser(request: EditUserRequest): Empty = authorized { + override suspend fun setUser(request: EditUserRequest): Empty { val result = editUserUseCase.execute( + getAuthorization(), User.Patch( name = UserName.createOrFail(request.name), description = UserDescription.createOrFail(request.description), @@ -58,8 +60,9 @@ class UsersService( ) ) - when (result) { + return when (result) { EditUserUseCase.Result.Success -> Empty.Default + EditUserUseCase.Result.Failed -> internalFailure() } } diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/RemoveTimerRequest.proto b/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/RemoveTimerRequest.proto deleted file mode 100644 index 10684d59..00000000 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/RemoveTimerRequest.proto +++ /dev/null @@ -1,9 +0,0 @@ -syntax = "proto3"; - -import "io/timemates/api/timers/types/Timer.proto"; - -option java_package = "io.timemates.api.timers.requests"; - -message RemoveTimerRequest { - int64 timerId = 1; -} \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/GetCurrentTimerSessionRequest.proto b/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/GetCurrentTimerSessionRequest.proto deleted file mode 100644 index 150599d0..00000000 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/GetCurrentTimerSessionRequest.proto +++ /dev/null @@ -1,11 +0,0 @@ -syntax = "proto3"; - -import "io/timemates/api/timers/types/Timer.proto"; - -option java_package = "io.timemates.api.timers.requests"; - -message GetCurrentTimerSessionRequest { - message Response { - Timer timer = 1; - } -} \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/AuthorizationService.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/AuthorizationService.proto similarity index 70% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/AuthorizationService.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/AuthorizationService.proto index c27c867e..8d1a9427 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/AuthorizationService.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/AuthorizationService.proto @@ -1,15 +1,15 @@ syntax = "proto3"; -import "io/timemates/api/authorizations/requests/StartAuthorizationRequest.proto"; -import "io/timemates/api/authorizations/requests/ConfirmAuthorizationRequest.proto"; -import "io/timemates/api/authorizations/requests/GetAuthorizationsRequest.proto"; -import "io/timemates/api/authorizations/options/OmitAuthorizationOption.proto"; -import "io/timemates/api/authorizations/requests/CreateProfileRequest.proto"; -import "io/timemates/api/authorizations/requests/RenewAuthorizationRequest.proto"; -import "io/timemates/api/authorizations/types/Authorization.proto"; +import "org/timemates/api/authorizations/requests/StartAuthorizationRequest.proto"; +import "org/timemates/api/authorizations/requests/ConfirmAuthorizationRequest.proto"; +import "org/timemates/api/authorizations/requests/GetAuthorizationsRequest.proto"; +import "org/timemates/api/authorizations/options/OmitAuthorizationOption.proto"; +import "org/timemates/api/authorizations/requests/CreateProfileRequest.proto"; +import "org/timemates/api/authorizations/requests/RenewAuthorizationRequest.proto"; +import "org/timemates/api/authorizations/types/Authorization.proto"; import "google/protobuf/empty.proto"; -option java_package = "io.timemates.api.authorizations"; +option java_package = "org.timemates.api.authorizations"; service AuthorizationService { /** diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/options/OmitAuthorizationOption.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/options/OmitAuthorizationOption.proto similarity index 82% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/options/OmitAuthorizationOption.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/options/OmitAuthorizationOption.proto index e1f69d36..a001e6be 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/options/OmitAuthorizationOption.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/options/OmitAuthorizationOption.proto @@ -2,7 +2,7 @@ syntax = "proto3"; import "google/protobuf/descriptor.proto"; -option java_package = "io.timemates.api.authorizations.options"; +option java_package = "org.timemates.api.authorizations.options"; extend google.protobuf.MethodOptions { /** diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/ConfirmAuthorizationRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/ConfirmAuthorizationRequest.proto similarity index 67% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/ConfirmAuthorizationRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/ConfirmAuthorizationRequest.proto index e34d9509..8e153c25 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/ConfirmAuthorizationRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/ConfirmAuthorizationRequest.proto @@ -1,7 +1,7 @@ syntax = "proto3"; -import "io/timemates/api/authorizations/types/Authorization.proto"; +import "org/timemates/api/authorizations/types/Authorization.proto"; -option java_package = "io.timemates.api.authorizations.requests"; +option java_package = "org.timemates.api.authorizations.requests"; message ConfirmAuthorizationRequest { string verificationHash = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/CreateProfileRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/CreateProfileRequest.proto similarity index 67% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/CreateProfileRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/CreateProfileRequest.proto index b01a69aa..7d693ea3 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/CreateProfileRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/CreateProfileRequest.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -import "io/timemates/api/authorizations/types/Authorization.proto"; +import "org/timemates/api/authorizations/types/Authorization.proto"; -option java_package = "io.timemates.api.users.requests"; +option java_package = "org.timemates.api.users.requests"; message CreateProfileRequest { string verificationHash = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/GetAuthorizationsRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/GetAuthorizationsRequest.proto similarity index 62% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/GetAuthorizationsRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/GetAuthorizationsRequest.proto index bb46c28a..c7165d31 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/GetAuthorizationsRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/GetAuthorizationsRequest.proto @@ -1,7 +1,7 @@ syntax = "proto3"; -import "io/timemates/api/authorizations/types/Authorization.proto"; +import "org/timemates/api/authorizations/types/Authorization.proto"; -option java_package = "io.timemates.api.authorizations.requests"; +option java_package = "org.timemates.api.authorizations.requests"; message GetAuthorizationsRequest { // null if it's start of pagination diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/RenewAuthorizationRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/RenewAuthorizationRequest.proto similarity index 52% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/RenewAuthorizationRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/RenewAuthorizationRequest.proto index f5ad398a..4c13cb89 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/RenewAuthorizationRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/RenewAuthorizationRequest.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -import "io/timemates/api/authorizations/types/Authorization.proto"; +import "org/timemates/api/authorizations/types/Authorization.proto"; -option java_package = "io.timemates.api.authorizations.requests"; +option java_package = "org.timemates.api.authorizations.requests"; message RenewAuthorizationRequest { string refreshHash = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/StartAuthorizationRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/StartAuthorizationRequest.proto similarity index 53% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/StartAuthorizationRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/StartAuthorizationRequest.proto index 1992b67c..576d1984 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/requests/StartAuthorizationRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/requests/StartAuthorizationRequest.proto @@ -1,9 +1,9 @@ syntax = "proto3"; -import "io/timemates/api/authorizations/types/Authorization.proto"; -import "io/timemates/api/authorizations/types/Metadata.proto"; +import "org/timemates/api/authorizations/types/Authorization.proto"; +import "org/timemates/api/authorizations/types/Metadata.proto"; -option java_package = "io.timemates.api.authorizations.requests"; +option java_package = "org.timemates.api.authorizations.requests"; message StartAuthorizationRequest { string emailAddress = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/types/Authorization.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/types/Authorization.proto similarity index 82% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/types/Authorization.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/types/Authorization.proto index a7908e3d..46a5d928 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/types/Authorization.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/types/Authorization.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -import "io/timemates/api/authorizations/types/Metadata.proto"; +import "org/timemates/api/authorizations/types/Metadata.proto"; -option java_package = "io.timemates.api.authorizations.types"; +option java_package = "org.timemates.api.authorizations.types"; message Authorization { /** diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/types/Metadata.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/types/Metadata.proto similarity index 59% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/types/Metadata.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/types/Metadata.proto index 75d00a1a..bfb0ecf7 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/authorizations/types/Metadata.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/authorizations/types/Metadata.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.authorizations.types"; +option java_package = "org.timemates.api.authorizations.types"; message Metadata { string clientName = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/TimersService.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/TimersService.proto similarity index 59% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/TimersService.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/TimersService.proto index 3300089e..f6cbac94 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/TimersService.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/TimersService.proto @@ -1,20 +1,20 @@ syntax = "proto3"; -import "io/timemates/api/timers/requests/CreateTimerRequest.proto"; -import "io/timemates/api/timers/requests/EditTimerInfoRequest.proto"; -import "io/timemates/api/timers/requests/RemoveTimerRequest.proto"; -import "io/timemates/api/timers/requests/GetTimerRequest.proto"; -import "io/timemates/api/timers/requests/GetTimersRequest.proto"; -import "io/timemates/api/timers/members/requests/KickMemberRequest.proto"; -import "io/timemates/api/timers/members/requests/GetMembersRequest.proto"; -import "io/timemates/api/timers/members/invites/requests/CreateInviteRequest.proto"; -import "io/timemates/api/timers/members/invites/requests/GetInvitesRequest.proto"; -import "io/timemates/api/timers/members/invites/requests/RemoveInviteRequest.proto"; -import "io/timemates/api/timers/members/invites/requests/JoinTimerByInviteCodeRequest.proto"; -import "io/timemates/api/timers/types/Timer.proto"; +import "org/timemates/api/timers/requests/CreateTimerRequest.proto"; +import "org/timemates/api/timers/requests/EditTimerInfoRequest.proto"; +import "org/timemates/api/timers/requests/RemoveTimerRequest.proto"; +import "org/timemates/api/timers/requests/GetTimerRequest.proto"; +import "org/timemates/api/timers/requests/GetTimersRequest.proto"; +import "org/timemates/api/timers/members/requests/KickMemberRequest.proto"; +import "org/timemates/api/timers/members/requests/GetMembersRequest.proto"; +import "org/timemates/api/timers/members/invites/requests/CreateInviteRequest.proto"; +import "org/timemates/api/timers/members/invites/requests/GetInvitesRequest.proto"; +import "org/timemates/api/timers/members/invites/requests/RemoveInviteRequest.proto"; +import "org/timemates/api/timers/members/invites/requests/JoinTimerByInviteCodeRequest.proto"; +import "org/timemates/api/timers/types/Timer.proto"; import "google/protobuf/empty.proto"; -option java_package = "io.timemates.api.timers"; +option java_package = "org.timemates.api.timers"; service TimersService { /** diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/CreateInviteRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/CreateInviteRequest.proto similarity index 54% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/CreateInviteRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/CreateInviteRequest.proto index bbeead25..1f2617d1 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/CreateInviteRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/CreateInviteRequest.proto @@ -1,7 +1,7 @@ syntax = "proto3"; -import "io/timemates/api/users/types/User.proto"; +import "org/timemates/api/users/types/User.proto"; -option java_package = "io.timemates.api.timers.members.invites.requests"; +option java_package = "org.timemates.api.timers.members.invites.requests"; message InviteMemberRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/GetInvitesRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/GetInvitesRequest.proto similarity index 58% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/GetInvitesRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/GetInvitesRequest.proto index 3947777f..b0687709 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/GetInvitesRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/GetInvitesRequest.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -import "io/timemates/api/timers/members/invites/types/Invite.proto"; +import "org/timemates/api/timers/members/invites/types/Invite.proto"; -option java_package = "io.timemates.api.timers.members.invites.requests"; +option java_package = "org.timemates.api.timers.members.invites.requests"; message GetInvitesRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/JoinTimerByInviteCodeRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/JoinTimerByInviteCodeRequest.proto similarity index 54% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/JoinTimerByInviteCodeRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/JoinTimerByInviteCodeRequest.proto index 448bb0ef..63821c81 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/JoinTimerByInviteCodeRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/JoinTimerByInviteCodeRequest.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -import "io/timemates/api/timers/types/Timer.proto"; +import "org/timemates/api/timers/types/Timer.proto"; -option java_package = "io.timemates.api.timers.requests"; +option java_package = "org.timemates.api.timers.requests"; message JoinTimerByInviteCodeRequest { string inviteCode = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/RemoveInviteRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/RemoveInviteRequest.proto similarity index 56% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/RemoveInviteRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/RemoveInviteRequest.proto index 82b33250..9703bfff 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/requests/RemoveInviteRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/requests/RemoveInviteRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.members.invites.requests"; +option java_package = "org.timemates.api.timers.members.invites.requests"; message RemoveInviteRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/types/Invite.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/types/Invite.proto similarity index 58% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/types/Invite.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/types/Invite.proto index 77172389..4fcacb9f 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/invites/types/Invite.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/invites/types/Invite.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.members.invites.types"; +option java_package = "org.timemates.api.timers.members.invites.types"; message Invite { string code = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/requests/GetMembersRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/requests/GetMembersRequest.proto similarity index 63% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/requests/GetMembersRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/requests/GetMembersRequest.proto index b9dee631..1e0d90b2 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/requests/GetMembersRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/requests/GetMembersRequest.proto @@ -1,7 +1,7 @@ syntax = "proto3"; -import "io/timemates/api/users/types/User.proto"; +import "org/timemates/api/users/types/User.proto"; -option java_package = "io.timemates.api.timers.members.requests"; +option java_package = "org.timemates.api.timers.members.requests"; message GetMembersRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/requests/KickMemberRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/requests/KickMemberRequest.proto similarity index 57% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/requests/KickMemberRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/requests/KickMemberRequest.proto index 13918efa..463e4cf0 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/members/requests/KickMemberRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/members/requests/KickMemberRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.members.requests"; +option java_package = "org.timemates.api.timers.members.requests"; message KickMemberRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/CreateTimerRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/CreateTimerRequest.proto similarity index 72% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/CreateTimerRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/CreateTimerRequest.proto index 144ff686..3539c978 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/CreateTimerRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/CreateTimerRequest.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -import "io/timemates/api/timers/types/Timer.proto"; +import "org/timemates/api/timers/types/Timer.proto"; -option java_package = "io.timemates.api.timers.requests"; +option java_package = "org.timemates.api.timers.requests"; message CreateTimerRequest { /** diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/EditTimerInfoRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/EditTimerInfoRequest.proto similarity index 73% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/EditTimerInfoRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/EditTimerInfoRequest.proto index 2d266b78..4cbe8a38 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/EditTimerInfoRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/EditTimerInfoRequest.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -import "io/timemates/api/timers/types/Timer.proto"; +import "org/timemates/api/timers/types/Timer.proto"; -option java_package = "io.timemates.api.timers.requests"; +option java_package = "org.timemates.api.timers.requests"; message EditTimerRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/GetTimerRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/GetTimerRequest.proto similarity index 53% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/GetTimerRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/GetTimerRequest.proto index 70143b6c..dfca8d81 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/GetTimerRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/GetTimerRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.requests"; +option java_package = "org.timemates.api.timers.requests"; message GetTimerRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/GetTimersRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/GetTimersRequest.proto similarity index 61% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/GetTimersRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/GetTimersRequest.proto index e409eeb1..210abfc8 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/requests/GetTimersRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/GetTimersRequest.proto @@ -1,7 +1,7 @@ syntax = "proto3"; -import "io/timemates/api/timers/types/Timer.proto"; +import "org/timemates/api/timers/types/Timer.proto"; -option java_package = "io.timemates.api.timers.requests"; +option java_package = "org.timemates.api.timers.requests"; message GetTimersRequest { optional string nextPageToken = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/RemoveTimerRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/RemoveTimerRequest.proto new file mode 100644 index 00000000..6b73bb2b --- /dev/null +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/requests/RemoveTimerRequest.proto @@ -0,0 +1,9 @@ +syntax = "proto3"; + +import "org/timemates/api/timers/types/Timer.proto"; + +option java_package = "org.timemates.api.timers.requests"; + +message RemoveTimerRequest { + int64 timerId = 1; +} \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/TimerSessionsService.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/TimerSessionsService.proto similarity index 74% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/TimerSessionsService.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/TimerSessionsService.proto index d34518d5..06a14c47 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/TimerSessionsService.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/TimerSessionsService.proto @@ -1,16 +1,16 @@ syntax = "proto3"; -import "io/timemates/api/timers/sessions/requests/StartTimerSessionRequest.proto"; -import "io/timemates/api/timers/sessions/requests/StopTimerSessionRequest.proto"; -import "io/timemates/api/timers/sessions/requests/JoinTimerSessionRequest.proto"; -import "io/timemates/api/timers/sessions/requests/ConfirmTimerSessionRequest.proto"; -import "io/timemates/api/timers/sessions/requests/GetTimerStateRequest.proto"; -import "io/timemates/api/timers/sessions/types/TimerState.proto"; +import "org/timemates/api/timers/sessions/requests/StartTimerSessionRequest.proto"; +import "org/timemates/api/timers/sessions/requests/StopTimerSessionRequest.proto"; +import "org/timemates/api/timers/sessions/requests/JoinTimerSessionRequest.proto"; +import "org/timemates/api/timers/sessions/requests/ConfirmTimerSessionRequest.proto"; +import "org/timemates/api/timers/sessions/requests/GetTimerStateRequest.proto"; +import "org/timemates/api/timers/sessions/types/TimerState.proto"; import "google/protobuf/empty.proto"; -import "io/timemates/api/timers/sessions/requests/GetCurrentTimerSessionRequest.proto"; -import "io/timemates/api/timers/types/Timer.proto"; +import "org/timemates/api/timers/sessions/requests/GetCurrentTimerSessionRequest.proto"; +import "org/timemates/api/timers/types/Timer.proto"; -option java_package = "io.timemates.api.timers"; +option java_package = "org.timemates.api.timers"; /** * The TimerSessionsService provides operations related to timer sessions. diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/ConfirmTimerSessionRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/ConfirmTimerSessionRequest.proto similarity index 54% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/ConfirmTimerSessionRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/ConfirmTimerSessionRequest.proto index 6fad59a2..e57e1961 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/ConfirmTimerSessionRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/ConfirmTimerSessionRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.sessions.requests"; +option java_package = "org.timemates.api.timers.sessions.requests"; message ConfirmTimerSessionRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/GetCurrentTimerSessionRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/GetCurrentTimerSessionRequest.proto new file mode 100644 index 00000000..465e2879 --- /dev/null +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/GetCurrentTimerSessionRequest.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +import "org/timemates/api/timers/types/Timer.proto"; + +option java_package = "org.timemates.api.timers.requests"; + +message GetCurrentTimerSessionRequest { + message Response { + Timer timer = 1; + } +} \ No newline at end of file diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/GetTimerStateRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/GetTimerStateRequest.proto similarity index 52% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/GetTimerStateRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/GetTimerStateRequest.proto index ef4e16c0..cde41650 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/GetTimerStateRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/GetTimerStateRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.sessions.requests"; +option java_package = "org.timemates.api.timers.sessions.requests"; message GetTimerStateRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/JoinTimerSessionRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/JoinTimerSessionRequest.proto similarity index 53% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/JoinTimerSessionRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/JoinTimerSessionRequest.proto index 209cc2f6..214fa3f5 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/JoinTimerSessionRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/JoinTimerSessionRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.sessions.requests"; +option java_package = "org.timemates.api.timers.sessions.requests"; message JoinTimerSessionRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/StartTimerSessionRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/StartTimerSessionRequest.proto similarity index 51% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/StartTimerSessionRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/StartTimerSessionRequest.proto index 168e5b69..dfc26ebb 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/StartTimerSessionRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/StartTimerSessionRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.sessions.requests"; +option java_package = "org.timemates.api.timers.sessions.requests"; message StartTimerRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/StopTimerSessionRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/StopTimerSessionRequest.proto similarity index 50% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/StopTimerSessionRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/StopTimerSessionRequest.proto index 15dcba09..8e403dfa 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/requests/StopTimerSessionRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/requests/StopTimerSessionRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.sessions.requests"; +option java_package = "org.timemates.api.timers.sessions.requests"; message StopTimerRequest { int64 timerId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/types/TimerState.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/types/TimerState.proto similarity index 96% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/types/TimerState.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/types/TimerState.proto index ebe39bf1..dd85e9ad 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/sessions/types/TimerState.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/sessions/types/TimerState.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.timers.sessions.types"; +option java_package = "org.timemates.api.timers.sessions.types"; /** * Represents the state of a timer. diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/types/Timer.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/types/Timer.proto similarity index 95% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/types/Timer.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/types/Timer.proto index cf607974..89ad05bf 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/timers/types/Timer.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/timers/types/Timer.proto @@ -1,8 +1,8 @@ syntax = "proto3"; -import "io/timemates/api/timers/sessions/types/TimerState.proto"; +import "org/timemates/api/timers/sessions/types/TimerState.proto"; -option java_package = "io.timemates.api.timers.types"; +option java_package = "org.timemates.api.timers.types"; /** * Represents a timer. diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/UsersService.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/UsersService.proto similarity index 64% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/UsersService.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/UsersService.proto index f9e65245..392f9e39 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/UsersService.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/UsersService.proto @@ -1,12 +1,12 @@ syntax = "proto3"; -import "io/timemates/api/users/requests/GetUsersRequest.proto"; -import "io/timemates/api/users/requests/EditUserRequest.proto"; -import "io/timemates/api/users/requests/EditEmailRequest.proto"; -import "io/timemates/api/users/types/User.proto"; +import "org/timemates/api/users/requests/GetUsersRequest.proto"; +import "org/timemates/api/users/requests/EditUserRequest.proto"; +import "org/timemates/api/users/requests/EditEmailRequest.proto"; +import "org/timemates/api/users/types/User.proto"; import "google/protobuf/empty.proto"; -option java_package = "io.timemates.api.users"; +option java_package = "org.timemates.api.users"; service UsersService { /** diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/EditEmailRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/EditEmailRequest.proto similarity index 54% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/EditEmailRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/EditEmailRequest.proto index d22dc873..95e1d3ce 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/EditEmailRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/EditEmailRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.users.requests"; +option java_package = "org.timemates.api.users.requests"; message EditEmailRequest { string email = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/EditUserRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/EditUserRequest.proto similarity index 80% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/EditUserRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/EditUserRequest.proto index 53b072cf..621b1dc4 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/EditUserRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/EditUserRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.users.requests"; +option java_package = "org.timemates.api.users.requests"; message EditUserRequest { /** diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/GetUsersRequest.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/GetUsersRequest.proto similarity index 57% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/GetUsersRequest.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/GetUsersRequest.proto index 2ab50573..515866bb 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/requests/GetUsersRequest.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/requests/GetUsersRequest.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.users.requests"; +option java_package = "org.timemates.api.users.requests"; message GetUsersRequest { repeated int64 userId = 1; diff --git a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/types/User.proto b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/types/User.proto similarity index 87% rename from infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/types/User.proto rename to infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/types/User.proto index 62e94379..6ee538ed 100644 --- a/infrastructure/rsocket-api/src/main/proto/io/timemates/api/users/types/User.proto +++ b/infrastructure/rsocket-api/src/main/proto/org/timemates/api/users/types/User.proto @@ -1,6 +1,6 @@ syntax = "proto3"; -option java_package = "io.timemates.api.users.types"; +option java_package = "org.timemates.api.users.types"; message User { /** diff --git a/settings.gradle.kts b/settings.gradle.kts index 4f27db97..9ae3b512 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -26,26 +26,48 @@ rootProject.name = "timemates-backend" includeBuild("build-conventions") include( - ":common:validation", - ":common:random", - ":common:authorization", - ":common:time", - ":common:exposed-utils", - ":common:test-utils", - ":common:state-machine", - ":common:coroutines-utils", - ":common:page-token", - ":common:smtp-mailer", - ":common:cli-arguments", - ":common:hashing", + ":foundation:validation", + ":foundation:validation:tests-integration", + ":foundation:random", + ":foundation:authorization", + ":foundation:time", + ":foundation:exposed-utils", + ":foundation:state-machine", + ":foundation:coroutines-utils", + ":foundation:page-token", + ":foundation:smtp-mailer", + ":foundation:cli-arguments", + ":foundation:hashing", ) -include(":core") - -include(":data") - include( ":infrastructure:rsocket-api", ) include(":app") + +include( + ":core:types", + ":core:types:auth-integration", +) + +include( + ":features:auth:domain", + ":features:auth:data", + ":features:auth:dependencies", + ":features:auth:adapters", +) + +include( + ":features:users:domain", + ":features:users:data", + ":features:users:dependencies", +) + + +include( + ":features:timers:domain", + ":features:timers:data", + ":features:timers:dependencies", + ":features:timers:adapters", +)