From a8fc61c0972f46e33ddfffd123ff78bfb727395b Mon Sep 17 00:00:00 2001 From: NathanFallet Date: Wed, 13 Dec 2023 13:26:49 +0100 Subject: [PATCH 1/2] Auth authorize API --- extopy-backend/build.gradle.kts | 6 +- .../extopy/controllers/auth/AuthRouter.kt | 26 ++++-- .../nathanfallet/extopy/database/Database.kt | 37 ++++---- .../extopy/database/application/Clients.kt | 34 +++++++ .../application/DatabaseClientsRepository.kt | 22 +++++ .../extopy/database/users/ClientsInUsers.kt | 33 +++++++ .../users/DatabaseClientsInUsersRepository.kt | 44 +++++++++ .../extopy/database/users/Users.kt | 7 -- .../me/nathanfallet/extopy/plugins/Koin.kt | 40 ++++++++ .../nathanfallet/extopy/plugins/Security.kt | 3 - .../users/IClientsInUsersRepository.kt | 12 +++ .../extopy/services/jwt/IJWTService.kt | 7 ++ .../extopy/services/jwt/JWTService.kt | 27 ++++++ .../usecases/auth/CreateAuthCodeUseCase.kt | 24 +++++ .../auth/CreateSessionForUserUseCase.kt | 2 +- .../usecases/auth/DeleteAuthCodeUseCase.kt | 14 +++ .../usecases/auth/GenerateAuthTokenUseCase.kt | 22 +++++ .../usecases/auth/GetAuthCodeUseCase.kt | 25 +++++ .../usecases/auth/GetSessionForCallUseCase.kt | 2 +- .../usecases/auth/SetSessionForCallUseCase.kt | 2 +- .../src/commonMain/resources/application.conf | 1 - .../resources/application.test.conf | 1 - .../DatabaseClientsRepositoryTest.kt | 33 +++++++ .../DatabaseClientsInUsersRepositoryTest.kt | 92 +++++++++++++++++++ .../extopy/services/jwt/JWTServiceTest.kt | 19 ++++ .../auth/CreateAuthCodeUseCaseTest.kt | 51 ++++++++++ .../auth/DeleteAuthCodeUseCaseTest.kt | 28 ++++++ .../auth/GenerateAuthTokenUseCaseTest.kt | 37 ++++++++ .../usecases/auth/GetAuthCodeUseCaseTest.kt | 82 +++++++++++++++++ extopy-commons/build.gradle.kts | 2 +- .../extopy/models/application/Client.kt | 19 ++++ .../extopy/models/auth/SessionPayload.kt | 2 +- .../nathanfallet/extopy/models/posts/Post.kt | 15 +++ .../extopy/models/posts/PostPayload.kt | 4 + .../extopy/models/users/ClientInUser.kt | 12 +++ .../extopy/models/users/UpdateUserPayload.kt | 7 ++ .../nathanfallet/extopy/models/users/User.kt | 18 ++++ .../extopy/models/users/UserToken.kt | 6 -- 38 files changed, 765 insertions(+), 53 deletions(-) create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/Clients.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepository.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/ClientsInUsers.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/DatabaseClientsInUsersRepository.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/repositories/users/IClientsInUsersRepository.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/services/jwt/IJWTService.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/services/jwt/JWTService.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCase.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/DeleteAuthCodeUseCase.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCase.kt create mode 100644 extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCase.kt create mode 100644 extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepositoryTest.kt create mode 100644 extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/users/DatabaseClientsInUsersRepositoryTest.kt create mode 100644 extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/services/jwt/JWTServiceTest.kt create mode 100644 extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCaseTest.kt create mode 100644 extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/DeleteAuthCodeUseCaseTest.kt create mode 100644 extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCaseTest.kt create mode 100644 extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCaseTest.kt create mode 100644 extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/application/Client.kt create mode 100644 extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/ClientInUser.kt delete mode 100644 extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/UserToken.kt diff --git a/extopy-backend/build.gradle.kts b/extopy-backend/build.gradle.kts index cc1c98d..dc45624 100644 --- a/extopy-backend/build.gradle.kts +++ b/extopy-backend/build.gradle.kts @@ -28,7 +28,7 @@ kotlin { val koinVersion = "3.5.0" val exposedVersion = "0.40.1" val logbackVersion = "0.9.30" - val ktorxVersion = "1.7.0" + val ktorxVersion = "1.7.3" sourceSets { val commonMain by getting { @@ -60,13 +60,13 @@ kotlin { implementation("ch.qos.logback:logback-core:$logbackVersion") implementation("ch.qos.logback:logback-classic:$logbackVersion") - implementation("me.nathanfallet.i18n:i18n:1.0.5") + implementation("me.nathanfallet.i18n:i18n:1.0.7") implementation("me.nathanfallet.ktorx:ktor-i18n:$ktorxVersion") implementation("me.nathanfallet.ktorx:ktor-i18n-freemarker:$ktorxVersion") implementation("me.nathanfallet.ktorx:ktor-routers:$ktorxVersion") implementation("me.nathanfallet.ktorx:ktor-routers-locale:$ktorxVersion") implementation("me.nathanfallet.ktorx:ktor-sentry:$ktorxVersion") - implementation("me.nathanfallet.cloudflare:cloudflare-api-client:4.0.8") + implementation("me.nathanfallet.cloudflare:cloudflare-api-client:4.0.10") implementation("com.mysql:mysql-connector-j:8.0.33") implementation("at.favre.lib:bcrypt:0.9.0") diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt index d581aff..58f96e7 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt @@ -6,18 +6,28 @@ import me.nathanfallet.extopy.models.auth.RegisterCodePayload import me.nathanfallet.extopy.models.auth.RegisterPayload import me.nathanfallet.ktorx.controllers.auth.IAuthWithCodeController import me.nathanfallet.ktorx.models.auth.AuthMapping +import me.nathanfallet.ktorx.routers.auth.AuthAPIRouter import me.nathanfallet.ktorx.routers.auth.LocalizedAuthWithCodeTemplateRouter +import me.nathanfallet.ktorx.routers.concat.ConcatUnitRouter import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase class AuthRouter( controller: IAuthWithCodeController, getLocaleForCallUseCase: IGetLocaleForCallUseCase, -) : LocalizedAuthWithCodeTemplateRouter( - LoginPayload::class, - RegisterPayload::class, - RegisterCodePayload::class, - AuthMapping(loginTemplate = "auth/login.ftl", registerTemplate = "auth/register.ftl"), - { template, model -> respondTemplate(template, model) }, - controller, - getLocaleForCallUseCase +) : ConcatUnitRouter( + listOf( + LocalizedAuthWithCodeTemplateRouter( + LoginPayload::class, + RegisterPayload::class, + RegisterCodePayload::class, + AuthMapping(loginTemplate = "auth/login.ftl", registerTemplate = "auth/register.ftl"), + { template, model -> respondTemplate(template, model) }, + controller, + getLocaleForCallUseCase + ), + AuthAPIRouter( + controller, + prefix = "/api/v1" + ) + ) ) diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/Database.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/Database.kt index 827ad72..bc3a763 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/Database.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/Database.kt @@ -1,11 +1,13 @@ package me.nathanfallet.extopy.database import kotlinx.coroutines.Dispatchers +import me.nathanfallet.extopy.database.application.Clients import me.nathanfallet.extopy.database.application.CodesInEmails import me.nathanfallet.extopy.database.notifications.Notifications import me.nathanfallet.extopy.database.notifications.TokensInNotifications import me.nathanfallet.extopy.database.posts.LikesInPosts import me.nathanfallet.extopy.database.posts.Posts +import me.nathanfallet.extopy.database.users.ClientsInUsers import me.nathanfallet.extopy.database.users.FollowersInUsers import me.nathanfallet.extopy.database.users.Users import org.jetbrains.exposed.sql.SchemaUtils @@ -20,34 +22,31 @@ class Database( password: String = "", ) { - private val database: org.jetbrains.exposed.sql.Database + // Connect to database + private val database: org.jetbrains.exposed.sql.Database = when (protocol) { + "mysql" -> org.jetbrains.exposed.sql.Database.connect( + "jdbc:mysql://$host:3306/$name", "com.mysql.cj.jdbc.Driver", + user, password + ) - init { - // Connect to database - database = when (protocol) { - "mysql" -> org.jetbrains.exposed.sql.Database.connect( - "jdbc:mysql://$host:3306/$name", "com.mysql.cj.jdbc.Driver", - user, password - ) - - "h2" -> org.jetbrains.exposed.sql.Database.connect( - "jdbc:h2:mem:$name;DB_CLOSE_DELAY=-1;", "org.h2.Driver" - ) + "h2" -> org.jetbrains.exposed.sql.Database.connect( + "jdbc:h2:mem:$name;DB_CLOSE_DELAY=-1;", "org.h2.Driver" + ) - else -> throw Exception("Unsupported database protocol: $protocol") - } + else -> throw Exception("Unsupported database protocol: $protocol") + } - // Create tables (if needed) + init { transaction(database) { - //SchemaUtils.create(Authorizes) - //SchemaUtils.create(Clients) + SchemaUtils.create(Clients) SchemaUtils.create(CodesInEmails) + SchemaUtils.create(Users) + SchemaUtils.create(ClientsInUsers) + SchemaUtils.create(FollowersInUsers) SchemaUtils.create(Notifications) SchemaUtils.create(TokensInNotifications) SchemaUtils.create(Posts) SchemaUtils.create(LikesInPosts) - SchemaUtils.create(Users) - SchemaUtils.create(FollowersInUsers) } } diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/Clients.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/Clients.kt new file mode 100644 index 0000000..10a5352 --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/Clients.kt @@ -0,0 +1,34 @@ +package me.nathanfallet.extopy.database.application + +import me.nathanfallet.extopy.extensions.generateId +import me.nathanfallet.extopy.models.application.Client +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.select + +object Clients : Table() { + + val id = varchar("id", 32) + val ownerId = varchar("owner_id", 32) + val name = varchar("name", 255) + val secret = varchar("secret", 255) + val redirectUri = text("redirect_uri") + + override val primaryKey = PrimaryKey(id) + + fun generateId(): String { + val candidate = String.generateId() + return if (select { id eq candidate }.count() > 0) generateId() else candidate + } + + fun toClient( + row: ResultRow, + ) = Client( + row[id], + row[ownerId], + row[name], + row[secret], + row[redirectUri] + ) + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepository.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepository.kt new file mode 100644 index 0000000..7acb0d2 --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepository.kt @@ -0,0 +1,22 @@ +package me.nathanfallet.extopy.database.application + +import me.nathanfallet.extopy.database.Database +import me.nathanfallet.extopy.models.application.Client +import me.nathanfallet.usecases.context.IContext +import me.nathanfallet.usecases.models.repositories.IModelSuspendRepository +import org.jetbrains.exposed.sql.select + +class DatabaseClientsRepository( + private val database: Database, +) : IModelSuspendRepository { + + override suspend fun get(id: String, context: IContext?): Client? { + return database.dbQuery { + Clients + .select { Clients.id eq id } + .map(Clients::toClient) + .singleOrNull() + } + } + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/ClientsInUsers.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/ClientsInUsers.kt new file mode 100644 index 0000000..498e926 --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/ClientsInUsers.kt @@ -0,0 +1,33 @@ +package me.nathanfallet.extopy.database.users + +import kotlinx.datetime.toInstant +import me.nathanfallet.extopy.extensions.generateId +import me.nathanfallet.extopy.models.users.ClientInUser +import org.jetbrains.exposed.sql.ResultRow +import org.jetbrains.exposed.sql.Table +import org.jetbrains.exposed.sql.select + +object ClientsInUsers : Table() { + + val code = varchar("code", 32) + val userId = varchar("user_id", 32) + val clientId = varchar("client_id", 32) + val expiration = varchar("expiration", 255) + + override val primaryKey = PrimaryKey(code) + + fun generateCode(): String { + val candidate = String.generateId() + return if (select { code eq candidate }.count() > 0) generateCode() else candidate + } + + fun toClientInUser( + row: ResultRow, + ) = ClientInUser( + row[code], + row[userId], + row[clientId], + row[expiration].toInstant() + ) + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/DatabaseClientsInUsersRepository.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/DatabaseClientsInUsersRepository.kt new file mode 100644 index 0000000..a7d7ab8 --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/DatabaseClientsInUsersRepository.kt @@ -0,0 +1,44 @@ +package me.nathanfallet.extopy.database.users + +import kotlinx.datetime.Instant +import me.nathanfallet.extopy.database.Database +import me.nathanfallet.extopy.models.users.ClientInUser +import me.nathanfallet.extopy.repositories.users.IClientsInUsersRepository +import org.jetbrains.exposed.sql.Op +import org.jetbrains.exposed.sql.deleteWhere +import org.jetbrains.exposed.sql.insert +import org.jetbrains.exposed.sql.select + +class DatabaseClientsInUsersRepository( + private val database: Database, +) : IClientsInUsersRepository { + + override suspend fun create(userId: String, clientId: String, expiration: Instant): ClientInUser? { + return database.dbQuery { + ClientsInUsers.insert { + it[code] = generateCode() + it[ClientsInUsers.userId] = userId + it[ClientsInUsers.clientId] = clientId + it[ClientsInUsers.expiration] = expiration.toString() + }.resultedValues?.map(ClientsInUsers::toClientInUser)?.singleOrNull() + } + } + + override suspend fun get(code: String): ClientInUser? { + return database.dbQuery { + ClientsInUsers + .select { ClientsInUsers.code eq code } + .map(ClientsInUsers::toClientInUser) + .singleOrNull() + } + } + + override suspend fun delete(code: String): Boolean { + return database.dbQuery { + ClientsInUsers.deleteWhere { + Op.build { ClientsInUsers.code eq code } + } + } == 1 + } + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/Users.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/Users.kt index cd600b7..1a5b24e 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/Users.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/users/Users.kt @@ -2,7 +2,6 @@ package me.nathanfallet.extopy.database.users import kotlinx.datetime.toInstant import kotlinx.datetime.toLocalDate -import kotlinx.serialization.Serializable import me.nathanfallet.extopy.database.posts.Posts import me.nathanfallet.extopy.extensions.generateId import me.nathanfallet.extopy.models.users.User @@ -65,9 +64,3 @@ object Users : Table() { ) } - -@Serializable -data class UserAuthorize(val client_id: String, val client_secret: String, val code: String) - -//@Serializable -//data class UserAuthorizeRequest(val client: Client, val user: User) diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/plugins/Koin.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/plugins/Koin.kt index b295078..e8c0164 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/plugins/Koin.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/plugins/Koin.kt @@ -8,9 +8,12 @@ import me.nathanfallet.extopy.controllers.posts.PostsRouter import me.nathanfallet.extopy.controllers.users.UsersController import me.nathanfallet.extopy.controllers.users.UsersRouter import me.nathanfallet.extopy.database.Database +import me.nathanfallet.extopy.database.application.DatabaseClientsRepository import me.nathanfallet.extopy.database.application.DatabaseCodesInEmailsRepository import me.nathanfallet.extopy.database.posts.DatabasePostsRepository +import me.nathanfallet.extopy.database.users.DatabaseClientsInUsersRepository import me.nathanfallet.extopy.database.users.DatabaseUsersRepository +import me.nathanfallet.extopy.models.application.Client import me.nathanfallet.extopy.models.auth.LoginPayload import me.nathanfallet.extopy.models.auth.RegisterCodePayload import me.nathanfallet.extopy.models.auth.RegisterPayload @@ -21,9 +24,12 @@ import me.nathanfallet.extopy.models.users.UpdateUserPayload import me.nathanfallet.extopy.models.users.User import me.nathanfallet.extopy.repositories.application.ICodesInEmailsRepository import me.nathanfallet.extopy.repositories.posts.IPostsRepository +import me.nathanfallet.extopy.repositories.users.IClientsInUsersRepository import me.nathanfallet.extopy.repositories.users.IUsersRepository import me.nathanfallet.extopy.services.emails.EmailsService import me.nathanfallet.extopy.services.emails.IEmailsService +import me.nathanfallet.extopy.services.jwt.IJWTService +import me.nathanfallet.extopy.services.jwt.JWTService import me.nathanfallet.extopy.usecases.application.SendEmailUseCase import me.nathanfallet.extopy.usecases.auth.* import me.nathanfallet.extopy.usecases.posts.CreatePostUseCase @@ -47,8 +53,11 @@ import me.nathanfallet.usecases.localization.ITranslateUseCase import me.nathanfallet.usecases.models.create.ICreateModelSuspendUseCase import me.nathanfallet.usecases.models.create.context.ICreateModelWithContextSuspendUseCase import me.nathanfallet.usecases.models.delete.IDeleteModelSuspendUseCase +import me.nathanfallet.usecases.models.get.GetModelFromRepositorySuspendUseCase +import me.nathanfallet.usecases.models.get.IGetModelSuspendUseCase import me.nathanfallet.usecases.models.get.context.GetModelWithContextFromRepositorySuspendUseCase import me.nathanfallet.usecases.models.get.context.IGetModelWithContextSuspendUseCase +import me.nathanfallet.usecases.models.repositories.IModelSuspendRepository import me.nathanfallet.usecases.models.update.IUpdateModelSuspendUseCase import org.koin.core.qualifier.named import org.koin.dsl.module @@ -75,10 +84,25 @@ fun Application.configureKoin() { environment.config.property("email.password").getString() ) } + single { + JWTService( + environment.config.property("jwt.secret").getString(), + environment.config.property("jwt.issuer").getString() + ) + } } val repositoryModule = module { + // Application single { DatabaseCodesInEmailsRepository(get()) } + single>(named()) { + DatabaseClientsRepository(get()) + } + + // Users single { DatabaseUsersRepository(get()) } + single { DatabaseClientsInUsersRepository(get()) } + + // Posts single { DatabasePostsRepository(get()) } } val useCaseModule = module { @@ -86,6 +110,9 @@ fun Application.configureKoin() { single { SendEmailUseCase(get()) } single { TranslateUseCase() } single { GetLocaleForCallUseCase() } + single>(named()) { + GetModelFromRepositorySuspendUseCase(get(named())) + } // Auth single { HashPasswordUseCase() } @@ -107,6 +134,13 @@ fun Application.configureKoin() { ) } single { DeleteCodeRegisterUseCase(get()) } + single { GetClientFromModelUseCase(get(named())) } + single { CreateAuthCodeUseCase(get()) } + single { GetAuthCodeUseCase(get(), get(), get(named())) } + single { DeleteAuthCodeUseCase(get()) } + single { + GenerateAuthTokenUseCase(get()) + } // Users single { RequireUserForCallUseCase(get()) } @@ -139,6 +173,12 @@ fun Application.configureKoin() { // Auth single> { AuthWithCodeController( + get(), + get(), + get(), + get(), + get(), + get(), get(), get(), get(), diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/plugins/Security.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/plugins/Security.kt index 6fc802e..62db05d 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/plugins/Security.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/plugins/Security.kt @@ -15,11 +15,8 @@ fun Application.configureSecurity() { this@configureSecurity.environment.config.property("jwt.secret").getString() val issuer = this@configureSecurity.environment.config.property("jwt.issuer").getString() - val audience = - this@configureSecurity.environment.config.property("jwt.audience").getString() verifier( JWT.require(Algorithm.HMAC256(secret)) - .withAudience(audience) .withIssuer(issuer) .build() ) diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/repositories/users/IClientsInUsersRepository.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/repositories/users/IClientsInUsersRepository.kt new file mode 100644 index 0000000..875f6da --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/repositories/users/IClientsInUsersRepository.kt @@ -0,0 +1,12 @@ +package me.nathanfallet.extopy.repositories.users + +import kotlinx.datetime.Instant +import me.nathanfallet.extopy.models.users.ClientInUser + +interface IClientsInUsersRepository { + + suspend fun create(userId: String, clientId: String, expiration: Instant): ClientInUser? + suspend fun get(code: String): ClientInUser? + suspend fun delete(code: String): Boolean + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/services/jwt/IJWTService.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/services/jwt/IJWTService.kt new file mode 100644 index 0000000..435343d --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/services/jwt/IJWTService.kt @@ -0,0 +1,7 @@ +package me.nathanfallet.extopy.services.jwt + +interface IJWTService { + + fun generateJWT(userId: String, clientId: String, type: String): String + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/services/jwt/JWTService.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/services/jwt/JWTService.kt new file mode 100644 index 0000000..13ee29c --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/services/jwt/JWTService.kt @@ -0,0 +1,27 @@ +package me.nathanfallet.extopy.services.jwt + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import java.util.* + +class JWTService( + private val secret: String, + private val issuer: String, + private val expiration: Long = 7 * 24 * 60 * 60 * 1000L, // 7 days + private val refreshExpiration: Long = 30 * 24 * 60 * 60 * 1000L, // 30 days +) : IJWTService { + + override fun generateJWT(userId: String, clientId: String, type: String): String { + val effectiveExpiration = when (type) { + "refresh" -> refreshExpiration + else -> expiration + } + return JWT.create() + .withSubject(userId) + .withAudience(clientId) + .withIssuer(issuer) + .withExpiresAt(Date(System.currentTimeMillis() + effectiveExpiration)) + .sign(Algorithm.HMAC256(secret)) + } + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCase.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCase.kt new file mode 100644 index 0000000..7b636f3 --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCase.kt @@ -0,0 +1,24 @@ +package me.nathanfallet.extopy.usecases.auth + +import kotlinx.datetime.Clock +import kotlinx.datetime.DateTimeUnit +import kotlinx.datetime.TimeZone +import kotlinx.datetime.plus +import me.nathanfallet.extopy.models.users.User +import me.nathanfallet.extopy.repositories.users.IClientsInUsersRepository +import me.nathanfallet.ktorx.models.auth.ClientForUser +import me.nathanfallet.ktorx.usecases.auth.ICreateAuthCodeUseCase + +class CreateAuthCodeUseCase( + private val repository: IClientsInUsersRepository, +) : ICreateAuthCodeUseCase { + + override suspend fun invoke(input: ClientForUser): String? { + return repository.create( + (input.user as User).id, + input.client.clientId, + Clock.System.now().plus(1, DateTimeUnit.HOUR, TimeZone.currentSystemDefault()) + )?.code + } + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/CreateSessionForUserUseCase.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/CreateSessionForUserUseCase.kt index 50ec9dc..b28fda0 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/CreateSessionForUserUseCase.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/CreateSessionForUserUseCase.kt @@ -3,7 +3,7 @@ package me.nathanfallet.extopy.usecases.auth import me.nathanfallet.extopy.models.auth.SessionPayload import me.nathanfallet.extopy.models.users.User import me.nathanfallet.ktorx.usecases.auth.ICreateSessionForUserUseCase -import me.nathanfallet.usecases.users.ISessionPayload +import me.nathanfallet.usecases.auth.ISessionPayload import me.nathanfallet.usecases.users.IUser class CreateSessionForUserUseCase : ICreateSessionForUserUseCase { diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/DeleteAuthCodeUseCase.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/DeleteAuthCodeUseCase.kt new file mode 100644 index 0000000..7e360fc --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/DeleteAuthCodeUseCase.kt @@ -0,0 +1,14 @@ +package me.nathanfallet.extopy.usecases.auth + +import me.nathanfallet.extopy.repositories.users.IClientsInUsersRepository +import me.nathanfallet.ktorx.usecases.auth.IDeleteAuthCodeUseCase + +class DeleteAuthCodeUseCase( + private val repository: IClientsInUsersRepository, +) : IDeleteAuthCodeUseCase { + + override suspend fun invoke(input: String): Boolean { + return repository.delete(input) + } + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCase.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCase.kt new file mode 100644 index 0000000..d85f090 --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCase.kt @@ -0,0 +1,22 @@ +package me.nathanfallet.extopy.usecases.auth + +import me.nathanfallet.extopy.models.users.User +import me.nathanfallet.extopy.services.jwt.IJWTService +import me.nathanfallet.ktorx.models.auth.ClientForUser +import me.nathanfallet.ktorx.usecases.auth.IGenerateAuthTokenUseCase +import me.nathanfallet.usecases.auth.AuthToken + +class GenerateAuthTokenUseCase( + private val jwtService: IJWTService, +) : IGenerateAuthTokenUseCase { + + override suspend fun invoke(input: ClientForUser): AuthToken { + val userId = (input.user as User).id + return AuthToken( + jwtService.generateJWT(userId, input.client.clientId, "access"), + jwtService.generateJWT(userId, input.client.clientId, "refresh"), + userId + ) + } + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCase.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCase.kt new file mode 100644 index 0000000..e1463d0 --- /dev/null +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCase.kt @@ -0,0 +1,25 @@ +package me.nathanfallet.extopy.usecases.auth + +import kotlinx.datetime.Clock +import me.nathanfallet.extopy.models.users.User +import me.nathanfallet.extopy.models.users.UserContext +import me.nathanfallet.extopy.repositories.users.IClientsInUsersRepository +import me.nathanfallet.ktorx.models.auth.ClientForUser +import me.nathanfallet.ktorx.usecases.auth.IGetAuthCodeUseCase +import me.nathanfallet.ktorx.usecases.auth.IGetClientUseCase +import me.nathanfallet.usecases.models.get.context.IGetModelWithContextSuspendUseCase + +class GetAuthCodeUseCase( + private val repository: IClientsInUsersRepository, + private val getClientUseCase: IGetClientUseCase, + private val getUserUseCase: IGetModelWithContextSuspendUseCase, +) : IGetAuthCodeUseCase { + + override suspend fun invoke(input: String): ClientForUser? { + val clientInUser = repository.get(input)?.takeIf { it.expiration > Clock.System.now() } ?: return null + val client = getClientUseCase(clientInUser.clientId) ?: return null + val user = getUserUseCase(clientInUser.userId, UserContext(clientInUser.userId)) ?: return null + return ClientForUser(client, user) + } + +} diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GetSessionForCallUseCase.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GetSessionForCallUseCase.kt index 4671edc..168c564 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GetSessionForCallUseCase.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/GetSessionForCallUseCase.kt @@ -4,7 +4,7 @@ import io.ktor.server.application.* import io.ktor.server.sessions.* import me.nathanfallet.extopy.models.auth.SessionPayload import me.nathanfallet.ktorx.usecases.auth.IGetSessionForCallUseCase -import me.nathanfallet.usecases.users.ISessionPayload +import me.nathanfallet.usecases.auth.ISessionPayload class GetSessionForCallUseCase : IGetSessionForCallUseCase { diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/SetSessionForCallUseCase.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/SetSessionForCallUseCase.kt index a2f1499..7eba250 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/SetSessionForCallUseCase.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/usecases/auth/SetSessionForCallUseCase.kt @@ -4,7 +4,7 @@ import io.ktor.server.application.* import io.ktor.server.sessions.* import me.nathanfallet.extopy.models.auth.SessionPayload import me.nathanfallet.ktorx.usecases.auth.ISetSessionForCallUseCase -import me.nathanfallet.usecases.users.ISessionPayload +import me.nathanfallet.usecases.auth.ISessionPayload class SetSessionForCallUseCase : ISetSessionForCallUseCase { diff --git a/extopy-backend/src/commonMain/resources/application.conf b/extopy-backend/src/commonMain/resources/application.conf index 0dd935a..a1ca841 100644 --- a/extopy-backend/src/commonMain/resources/application.conf +++ b/extopy-backend/src/commonMain/resources/application.conf @@ -24,7 +24,6 @@ jwt { secret = "secret" secret = ${?JWT_SECRET} issuer = "extopy" - audience = "extopy" } email { host = "mail.groupe-minaste.org" diff --git a/extopy-backend/src/commonMain/resources/application.test.conf b/extopy-backend/src/commonMain/resources/application.test.conf index 3621277..527e989 100644 --- a/extopy-backend/src/commonMain/resources/application.test.conf +++ b/extopy-backend/src/commonMain/resources/application.test.conf @@ -18,7 +18,6 @@ database { jwt { secret = "test" issuer = "extopy" - audience = "extopy" } email { host = "mail.groupe-minaste.org" diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepositoryTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepositoryTest.kt new file mode 100644 index 0000000..f2dc8b6 --- /dev/null +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepositoryTest.kt @@ -0,0 +1,33 @@ +package me.nathanfallet.extopy.database.application + +import kotlinx.coroutines.runBlocking +import me.nathanfallet.extopy.database.Database +import org.jetbrains.exposed.sql.insert +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class DatabaseClientsRepositoryTest { + + @Test + fun getClient() = runBlocking { + val database = Database(protocol = "h2", name = "getClient") + val repository = DatabaseClientsRepository(database) + val client = database.dbQuery { + Clients.insert { + it[id] = "id" + it[ownerId] = "ownerId" + it[name] = "name" + it[secret] = "secret" + it[redirectUri] = "redirectUri" + }.resultedValues?.map(Clients::toClient)?.singleOrNull() + } ?: fail("Unable to create client") + val result = repository.get(client.id) + assertEquals(client.id, result?.id) + assertEquals(client.ownerId, result?.ownerId) + assertEquals(client.name, result?.name) + assertEquals(client.secret, result?.secret) + assertEquals(client.redirectUri, result?.redirectUri) + } + +} diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/users/DatabaseClientsInUsersRepositoryTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/users/DatabaseClientsInUsersRepositoryTest.kt new file mode 100644 index 0000000..328bae5 --- /dev/null +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/users/DatabaseClientsInUsersRepositoryTest.kt @@ -0,0 +1,92 @@ +package me.nathanfallet.extopy.database.users + +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Clock +import me.nathanfallet.extopy.database.Database +import org.jetbrains.exposed.sql.selectAll +import kotlin.test.Test +import kotlin.test.assertEquals +import kotlin.test.fail + +class DatabaseClientsInUsersRepositoryTest { + + private val now = Clock.System.now() + + @Test + fun createClientInUser() = runBlocking { + val database = Database(protocol = "h2", name = "createClientInUser") + val repository = DatabaseClientsInUsersRepository(database) + val clientInUser = repository.create("userId", "clientId", now) + val clientInUserFromDatabase = database.dbQuery { + ClientsInUsers + .selectAll() + .map(ClientsInUsers::toClientInUser) + .singleOrNull() + } + assertEquals(clientInUserFromDatabase?.code, clientInUser?.code) + assertEquals(clientInUserFromDatabase?.userId, clientInUser?.userId) + assertEquals(clientInUserFromDatabase?.clientId, clientInUser?.clientId) + assertEquals(clientInUserFromDatabase?.expiration, clientInUser?.expiration) + assertEquals(clientInUserFromDatabase?.userId, "userId") + assertEquals(clientInUserFromDatabase?.clientId, "clientId") + assertEquals(clientInUserFromDatabase?.expiration, now) + } + + @Test + fun getClientInUser() = runBlocking { + val database = Database(protocol = "h2", name = "getClientInUser") + val repository = DatabaseClientsInUsersRepository(database) + val clientInUser = repository.create( + "userId", "clientId", now + ) ?: fail("Unable to create clientInUser") + val result = repository.get(clientInUser.code) + assertEquals(clientInUser.code, result?.code) + assertEquals(clientInUser.userId, result?.userId) + assertEquals(clientInUser.clientId, result?.clientId) + assertEquals(clientInUser.expiration, result?.expiration) + } + + @Test + fun getClientInUserNotFound() = runBlocking { + val database = Database(protocol = "h2", name = "getClientInUserNotFound") + val repository = DatabaseClientsInUsersRepository(database) + val result = repository.get("code") + assertEquals(null, result) + } + + @Test + fun deleteClientInUser() = runBlocking { + val database = Database(protocol = "h2", name = "deleteClientInUser") + val repository = DatabaseClientsInUsersRepository(database) + val clientInUser = repository.create( + "userId", "clientId", now + ) ?: fail("Unable to create clientInUser") + assertEquals(true, repository.delete(clientInUser.code)) + val count = database.dbQuery { + ClientsInUsers.selectAll().count() + } + assertEquals(0, count) + } + + @Test + fun deleteClientInUserNotFound() = runBlocking { + val database = Database(protocol = "h2", name = "deleteClientInUserNotFound") + val repository = DatabaseClientsInUsersRepository(database) + assertEquals(false, repository.delete("code")) + } + + @Test + fun deleteClientInUserWrong() = runBlocking { + val database = Database(protocol = "h2", name = "deleteClientInUserWrong") + val repository = DatabaseClientsInUsersRepository(database) + repository.create( + "userId", "clientId", now + ) ?: fail("Unable to create clientInUser") + assertEquals(false, repository.delete("code")) + val count = database.dbQuery { + ClientsInUsers.selectAll().count() + } + assertEquals(1, count) + } + +} diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/services/jwt/JWTServiceTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/services/jwt/JWTServiceTest.kt new file mode 100644 index 0000000..a74ae80 --- /dev/null +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/services/jwt/JWTServiceTest.kt @@ -0,0 +1,19 @@ +package me.nathanfallet.extopy.services.jwt + +import com.auth0.jwt.JWT +import com.auth0.jwt.algorithms.Algorithm +import kotlin.test.Test + +class JWTServiceTest { + + @Test + fun testGenerateJWT() { + val service = JWTService("secret", "issuer", 1000L, 2000L) + val token = service.generateJWT("uid", "cid", "access") + val verifier = JWT.require(Algorithm.HMAC256("secret")) + .withIssuer("issuer") + .build() + verifier.verify(token) + } + +} diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCaseTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCaseTest.kt new file mode 100644 index 0000000..1959dfe --- /dev/null +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCaseTest.kt @@ -0,0 +1,51 @@ +package me.nathanfallet.extopy.usecases.auth + +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.Clock +import me.nathanfallet.extopy.models.application.Client +import me.nathanfallet.extopy.models.users.ClientInUser +import me.nathanfallet.extopy.models.users.User +import me.nathanfallet.extopy.repositories.users.IClientsInUsersRepository +import me.nathanfallet.ktorx.models.auth.ClientForUser +import kotlin.test.Test +import kotlin.test.assertEquals + +class CreateAuthCodeUseCaseTest { + + @Test + fun testInvoke() = runBlocking { + val repository = mockk() + val useCase = CreateAuthCodeUseCase(repository) + coEvery { repository.create("uid", "cid", any()) } returns ClientInUser( + "code", "uid", "cid", Clock.System.now() + ) + assertEquals( + "code", + useCase( + ClientForUser( + Client("cid", "oid", "name", "secret", "redirect"), + User("uid", "displayName", "username") + ) + ) + ) + } + + @Test + fun testInvokeError() = runBlocking { + val repository = mockk() + val useCase = CreateAuthCodeUseCase(repository) + coEvery { repository.create("uid", "cid", any()) } returns null + assertEquals( + null, + useCase( + ClientForUser( + Client("cid", "oid", "name", "secret", "redirect"), + User("uid", "displayName", "username") + ) + ) + ) + } + +} diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/DeleteAuthCodeUseCaseTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/DeleteAuthCodeUseCaseTest.kt new file mode 100644 index 0000000..8dd8b1f --- /dev/null +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/DeleteAuthCodeUseCaseTest.kt @@ -0,0 +1,28 @@ +package me.nathanfallet.extopy.usecases.auth + +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import me.nathanfallet.extopy.repositories.users.IClientsInUsersRepository +import kotlin.test.Test +import kotlin.test.assertEquals + +class DeleteAuthCodeUseCaseTest { + + @Test + fun testInvokeTrue() = runBlocking { + val repository = mockk() + val useCase = DeleteAuthCodeUseCase(repository) + coEvery { repository.delete("code") } returns true + assertEquals(true, useCase("code")) + } + + @Test + fun testInvokeFalse() = runBlocking { + val repository = mockk() + val useCase = DeleteAuthCodeUseCase(repository) + coEvery { repository.delete("code") } returns false + assertEquals(false, useCase("code")) + } + +} diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCaseTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCaseTest.kt new file mode 100644 index 0000000..8b4526e --- /dev/null +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCaseTest.kt @@ -0,0 +1,37 @@ +package me.nathanfallet.extopy.usecases.auth + +import io.mockk.every +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import me.nathanfallet.extopy.models.application.Client +import me.nathanfallet.extopy.models.users.User +import me.nathanfallet.extopy.services.jwt.IJWTService +import me.nathanfallet.ktorx.models.auth.ClientForUser +import me.nathanfallet.usecases.auth.AuthToken +import kotlin.test.Test +import kotlin.test.assertEquals + +class GenerateAuthTokenUseCaseTest { + + @Test + fun testInvoke() = runBlocking { + val service = mockk() + val userCase = GenerateAuthTokenUseCase(service) + every { service.generateJWT("uid", "cid", "access") } returns "accessToken" + every { service.generateJWT("uid", "cid", "refresh") } returns "refreshToken" + assertEquals( + AuthToken( + "accessToken", + "refreshToken", + "uid" + ), + userCase( + ClientForUser( + Client("cid", "oid", "name", "secret", "redirect"), + User("uid", "displayName", "username") + ) + ) + ) + } + +} diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCaseTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCaseTest.kt new file mode 100644 index 0000000..69d44cf --- /dev/null +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCaseTest.kt @@ -0,0 +1,82 @@ +package me.nathanfallet.extopy.usecases.auth + +import io.mockk.coEvery +import io.mockk.mockk +import kotlinx.coroutines.runBlocking +import kotlinx.datetime.* +import me.nathanfallet.extopy.models.application.Client +import me.nathanfallet.extopy.models.users.ClientInUser +import me.nathanfallet.extopy.models.users.User +import me.nathanfallet.extopy.models.users.UserContext +import me.nathanfallet.extopy.repositories.users.IClientsInUsersRepository +import me.nathanfallet.ktorx.models.auth.ClientForUser +import me.nathanfallet.ktorx.usecases.auth.IGetClientUseCase +import me.nathanfallet.usecases.models.get.context.IGetModelWithContextSuspendUseCase +import kotlin.test.Test +import kotlin.test.assertEquals + +class GetAuthCodeUseCaseTest { + + private val now = Clock.System.now() + private val tomorrow = now.plus(1, DateTimeUnit.DAY, TimeZone.currentSystemDefault()) + private val yesterday = now.minus(1, DateTimeUnit.DAY, TimeZone.currentSystemDefault()) + + @Test + fun testInvoke() = runBlocking { + val repository = mockk() + val getClientUseCase = mockk() + val getUserUseCase = mockk>() + val useCase = GetAuthCodeUseCase(repository, getClientUseCase, getUserUseCase) + val clientForUser = ClientForUser( + Client("cid", "oid", "name", "secret", "redirect"), + User("uid", "displayName", "username") + ) + coEvery { repository.get("code") } returns ClientInUser("code", "uid", "cid", tomorrow) + coEvery { getClientUseCase("cid") } returns clientForUser.client + coEvery { getUserUseCase("uid", UserContext("uid")) } returns clientForUser.user as User + assertEquals(clientForUser, useCase("code")) + } + + @Test + fun testInvokeNotFound() = runBlocking { + val repository = mockk() + val useCase = GetAuthCodeUseCase(repository, mockk(), mockk()) + coEvery { repository.get("code") } returns null + assertEquals(null, useCase("code")) + } + + @Test + fun testInvokeExpired() = runBlocking { + val repository = mockk() + val useCase = GetAuthCodeUseCase(repository, mockk(), mockk()) + coEvery { repository.get("code") } returns ClientInUser("code", "uid", "cid", yesterday) + assertEquals(null, useCase("code")) + } + + @Test + fun testInvokeBadClient() = runBlocking { + val repository = mockk() + val getClientUseCase = mockk() + val useCase = GetAuthCodeUseCase(repository, getClientUseCase, mockk()) + coEvery { repository.get("code") } returns ClientInUser("code", "uid", "cid", tomorrow) + coEvery { getClientUseCase("cid") } returns null + assertEquals(null, useCase("code")) + } + + @Test + fun testInvokeBadUser() = runBlocking { + val repository = mockk() + val getClientUseCase = mockk() + val getUserUseCase = mockk>() + val useCase = GetAuthCodeUseCase(repository, getClientUseCase, getUserUseCase) + val clientForUser = ClientForUser( + Client("cid", "oid", "name", "secret", "redirect"), + User("uid", "displayName", "username") + ) + coEvery { repository.get("code") } returns ClientInUser("code", "uid", "cid", tomorrow) + coEvery { getClientUseCase("cid") } returns clientForUser.client + coEvery { getUserUseCase("uid", UserContext("uid")) } returns null + assertEquals(null, useCase("code")) + } + +} diff --git a/extopy-commons/build.gradle.kts b/extopy-commons/build.gradle.kts index 45cf36b..02f4a0e 100644 --- a/extopy-commons/build.gradle.kts +++ b/extopy-commons/build.gradle.kts @@ -59,7 +59,7 @@ kotlin { applyDefaultHierarchyTemplate() - val usecasesVersion = "1.5.1" + val usecasesVersion = "1.5.3" sourceSets { val commonMain by getting { diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/application/Client.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/application/Client.kt new file mode 100644 index 0000000..ab93547 --- /dev/null +++ b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/application/Client.kt @@ -0,0 +1,19 @@ +package me.nathanfallet.extopy.models.application + +import kotlinx.serialization.Serializable +import me.nathanfallet.usecases.auth.IClient +import me.nathanfallet.usecases.models.IModel + +@Serializable +data class Client( + override val id: String, + val ownerId: String, + val name: String, + val secret: String, + override val redirectUri: String, +) : IClient, IModel { + + override val clientId = id + override val clientSecret = secret + +} diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/auth/SessionPayload.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/auth/SessionPayload.kt index 0c7f287..dfb7e3c 100644 --- a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/auth/SessionPayload.kt +++ b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/auth/SessionPayload.kt @@ -1,7 +1,7 @@ package me.nathanfallet.extopy.models.auth import kotlinx.serialization.Serializable -import me.nathanfallet.usecases.users.ISessionPayload +import me.nathanfallet.usecases.auth.ISessionPayload @Serializable data class SessionPayload( diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/posts/Post.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/posts/Post.kt index 2b002f8..d1ac8b2 100644 --- a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/posts/Post.kt +++ b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/posts/Post.kt @@ -4,21 +4,36 @@ import kotlinx.datetime.Instant import kotlinx.serialization.Serializable import me.nathanfallet.extopy.models.users.User import me.nathanfallet.usecases.models.IModel +import me.nathanfallet.usecases.models.annotations.Schema @Serializable data class Post( + @Schema("Id of the Post", "123abc") override val id: String, + @Schema("Id of the User who posted", "123abc") val userId: String? = null, + @Schema("User who posted") val user: User? = null, + @Schema("Id of the Post replied to", "123abc") val repliedToId: String? = null, + @Schema("Id of the Post reposted", "123abc") val repostOfId: String? = null, + @Schema("Body of the Post", "Hello world!") val body: String? = null, + @Schema("Date of the Post", "2023-12-13T09:41:00Z") val published: Instant? = null, + @Schema("Date of the Post last edited, if edited", "2023-12-13T09:41:00Z") val edited: Instant? = null, + @Schema("Date of the Post expiration, if set", "2023-12-13T09:41:00Z") val expiration: Instant? = null, + @Schema("Visibility of the Post", "public") val visibility: String? = null, + @Schema("Number of likes of the Post", "123") val likesCount: Long? = null, + @Schema("Number of replies of the Post", "123") val repliesCount: Long? = null, + @Schema("Number of reposts of the Post", "123") val repostsCount: Long? = null, + @Schema("Is the current user has liked this post?", "true") val likesIn: Boolean? = null, ) : IModel diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/posts/PostPayload.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/posts/PostPayload.kt index 97f165f..e525d76 100644 --- a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/posts/PostPayload.kt +++ b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/posts/PostPayload.kt @@ -1,12 +1,16 @@ package me.nathanfallet.extopy.models.posts import kotlinx.serialization.Serializable +import me.nathanfallet.usecases.models.annotations.Schema import me.nathanfallet.usecases.models.annotations.validators.StringPropertyValidator @Serializable data class PostPayload( @StringPropertyValidator(maxLength = 20_000) + @Schema("Body of the Post", "Hello world!") val body: String, + @Schema("Id of the Post replied to", "123abc") val repliedToId: String? = null, + @Schema("Id of the Post reposted", "123abc") val repostOfId: String? = null, ) diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/ClientInUser.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/ClientInUser.kt new file mode 100644 index 0000000..f2c8d9d --- /dev/null +++ b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/ClientInUser.kt @@ -0,0 +1,12 @@ +package me.nathanfallet.extopy.models.users + +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable + +@Serializable +data class ClientInUser( + val code: String, + val userId: String, + val clientId: String, + val expiration: Instant, +) diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/UpdateUserPayload.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/UpdateUserPayload.kt index 29c2e6a..207cff0 100644 --- a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/UpdateUserPayload.kt +++ b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/UpdateUserPayload.kt @@ -1,16 +1,23 @@ package me.nathanfallet.extopy.models.users import kotlinx.serialization.Serializable +import me.nathanfallet.usecases.models.annotations.Schema import me.nathanfallet.usecases.models.annotations.validators.StringPropertyValidator @Serializable data class UpdateUserPayload( @StringPropertyValidator(regex = User.USERNAME_REGEX, maxLength = 25) + @Schema("Username of the User", "nathanfallet") val username: String? = null, @StringPropertyValidator(maxLength = 40) + @Schema("Display name of the User", "Nathan Fallet") val displayName: String? = null, + @Schema("Password", "abc123") val password: String? = null, + @Schema("Biography of the User", "Hi, I'm a developer") val biography: String? = null, + @Schema("Avatar of the User", "https://...") val avatar: String? = null, + @Schema("Is the User a personal (aka. private) account?", "false") val personal: Boolean? = null, ) diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/User.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/User.kt index b05387d..04b419e 100644 --- a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/User.kt +++ b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/User.kt @@ -4,27 +4,45 @@ import kotlinx.datetime.Instant import kotlinx.datetime.LocalDate import kotlinx.serialization.Serializable import me.nathanfallet.usecases.models.IModel +import me.nathanfallet.usecases.models.annotations.Schema import me.nathanfallet.usecases.users.IUser @Serializable data class User( + @Schema("Id of the User", "123abc") override val id: String, + @Schema("Display name of the User", "Nathan Fallet") val displayName: String, + @Schema("Username of the User", "nathanfallet") val username: String, + @Schema("Email of the User", "nathan@extopy.com") val email: String? = null, val password: String? = null, + @Schema("Biography of the User", "Hi, I'm a developer") val biography: String? = null, + @Schema("Avatar of the User", "https://...") val avatar: String? = null, + @Schema("Birthdate of the User", "2002-12-24") val birthdate: LocalDate? = null, + @Schema("Join date of the User", "2023-12-13T09:41:00Z") val joinDate: Instant? = null, + @Schema("Last active date of the User", "2023-12-13T09:41:00Z") val lastActive: Instant? = null, + @Schema("Is the User a personal (aka. private) account?", "false") val personal: Boolean? = null, + @Schema("Is the User verified?", "true") val verified: Boolean? = null, + @Schema("Is the User banned?", "false") val banned: Boolean? = null, + @Schema("Number of posts of the User", "123") val postsCount: Long? = null, + @Schema("Number of followers of the User", "123") val followersCount: Long? = null, + @Schema("Number of following of the User", "123") val followingCount: Long? = null, + @Schema("Is the current user following this user?", "true") val followersIn: Boolean? = null, + @Schema("Is the current user followed by this user?", "true") val followingIn: Boolean? = null, ) : IModel, IUser { diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/UserToken.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/UserToken.kt deleted file mode 100644 index 2d661d8..0000000 --- a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/users/UserToken.kt +++ /dev/null @@ -1,6 +0,0 @@ -package me.nathanfallet.extopy.models.users - -import kotlinx.serialization.Serializable - -@Serializable -data class UserToken(val token: String, val user: User) From c8fa173132945b7fd58690bd2578941e387cfc6c Mon Sep 17 00:00:00 2001 From: NathanFallet Date: Wed, 13 Dec 2023 15:29:39 +0100 Subject: [PATCH 2/2] Authorize template --- extopy-backend/build.gradle.kts | 2 +- .../extopy/controllers/auth/AuthRouter.kt | 8 +++- .../extopy/database/application/Clients.kt | 2 + .../resources/i18n/Messages_en.properties | 6 +++ .../resources/templates/auth/authorize.ftl | 38 ++++++++++--------- .../resources/templates/auth/template.ftl | 8 ++-- .../DatabaseClientsRepositoryTest.kt | 2 + .../extopy/services/jwt/JWTServiceTest.kt | 2 +- .../auth/CreateAuthCodeUseCaseTest.kt | 4 +- .../auth/GenerateAuthTokenUseCaseTest.kt | 2 +- .../usecases/auth/GetAuthCodeUseCaseTest.kt | 4 +- .../extopy/models/application/Client.kt | 1 + 12 files changed, 50 insertions(+), 29 deletions(-) diff --git a/extopy-backend/build.gradle.kts b/extopy-backend/build.gradle.kts index dc45624..26c46a9 100644 --- a/extopy-backend/build.gradle.kts +++ b/extopy-backend/build.gradle.kts @@ -28,7 +28,7 @@ kotlin { val koinVersion = "3.5.0" val exposedVersion = "0.40.1" val logbackVersion = "0.9.30" - val ktorxVersion = "1.7.3" + val ktorxVersion = "1.7.4" sourceSets { val commonMain by getting { diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt index 58f96e7..27c8477 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt @@ -20,7 +20,13 @@ class AuthRouter( LoginPayload::class, RegisterPayload::class, RegisterCodePayload::class, - AuthMapping(loginTemplate = "auth/login.ftl", registerTemplate = "auth/register.ftl"), + AuthMapping( + loginTemplate = "auth/login.ftl", + registerTemplate = "auth/register.ftl", + authorizeTemplate = "auth/authorize.ftl", + redirectTemplate = "auth/redirect.ftl", + redirectUnauthorizedToUrl = "/auth/login?redirect={path}", + ), { template, model -> respondTemplate(template, model) }, controller, getLocaleForCallUseCase diff --git a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/Clients.kt b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/Clients.kt index 10a5352..84afef5 100644 --- a/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/Clients.kt +++ b/extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/database/application/Clients.kt @@ -11,6 +11,7 @@ object Clients : Table() { val id = varchar("id", 32) val ownerId = varchar("owner_id", 32) val name = varchar("name", 255) + val description = text("description") val secret = varchar("secret", 255) val redirectUri = text("redirect_uri") @@ -27,6 +28,7 @@ object Clients : Table() { row[id], row[ownerId], row[name], + row[description], row[secret], row[redirectUri] ) diff --git a/extopy-backend/src/commonMain/resources/i18n/Messages_en.properties b/extopy-backend/src/commonMain/resources/i18n/Messages_en.properties index 41e1720..f51902d 100644 --- a/extopy-backend/src/commonMain/resources/i18n/Messages_en.properties +++ b/extopy-backend/src/commonMain/resources/i18n/Messages_en.properties @@ -3,6 +3,7 @@ error_internal=An internal error occurred auth_invalid_credentials=Invalid credentials auth_login_title=Sign in auth_register_title=Sign up +auth_authorize_title=Authorize {0} to access to your account? auth_register_code_created=Please check your inbox to complete your registration auth_register_email_taken=This email is already taken auth_register_email_title=Suite BDE - Registration @@ -13,6 +14,7 @@ auth_username_maxLength=Username is too long (max length is 25 characters) auth_displayName_maxLength=Display name is too long (max length is 40 characters) auth_reset_title=Reset my password auth_code_invalid=Invalid code +auth_invalid_client=Invalid client auth_field_email=Email address auth_field_username=Username auth_field_displayName=Display name @@ -20,10 +22,14 @@ auth_field_password=Password auth_field_birthdate=Birthdate auth_field_login=Sign in auth_field_join=Join +auth_field_authorize=Authorize auth_redirect=Redirecting\u2026 auth_hint_no_account=No account yet? auth_hint_password_forgotten=Forgot your password? auth_hint_already_have_account=Already have an account? +auth_hint_authorize_close=If you don't want to authorize this client, just close your browser. +auth_hint_authorize_connected_as=You are currently connected as: {0} +auth_hint_authorize_not_you=Not you? Use another account users_not_found=User not found users_create_not_allowed=You are not allowed to create users users_update_not_allowed=You are not allowed to update users diff --git a/extopy-backend/src/commonMain/resources/templates/auth/authorize.ftl b/extopy-backend/src/commonMain/resources/templates/auth/authorize.ftl index 298bd19..6fb7b87 100644 --- a/extopy-backend/src/commonMain/resources/templates/auth/authorize.ftl +++ b/extopy-backend/src/commonMain/resources/templates/auth/authorize.ftl @@ -1,23 +1,27 @@ <#import "template.ftl" as template> <@template.form> -

Authorize ${client.name} to access to your account?

- <#if error??> - + -
- ${client.description} -
+ <#if client??> +

<@t key="auth_authorize_title" args=[client.name] />

+ +
+ ${client.description} +
- -
- Connected as: ${user.displayname} (${user.username}) -
- -
- If you don't want to authorize this client, just close your browser. -
- \ No newline at end of file + +
+ <@t key="auth_hint_authorize_connected_as" args=["${user.displayName} (@${user.username})"] /> +
+ +
+ <@t key="auth_hint_authorize_close" /> +
+ + diff --git a/extopy-backend/src/commonMain/resources/templates/auth/template.ftl b/extopy-backend/src/commonMain/resources/templates/auth/template.ftl index 55fd6bb..69fae2c 100644 --- a/extopy-backend/src/commonMain/resources/templates/auth/template.ftl +++ b/extopy-backend/src/commonMain/resources/templates/auth/template.ftl @@ -4,19 +4,19 @@ - Authentification + Authentication - <#if redirectUrl??> - + <#if redirect??> +
- + logo <#nested>
diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepositoryTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepositoryTest.kt index f2dc8b6..af9a532 100644 --- a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepositoryTest.kt +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/database/application/DatabaseClientsRepositoryTest.kt @@ -18,6 +18,7 @@ class DatabaseClientsRepositoryTest { it[id] = "id" it[ownerId] = "ownerId" it[name] = "name" + it[description] = "description" it[secret] = "secret" it[redirectUri] = "redirectUri" }.resultedValues?.map(Clients::toClient)?.singleOrNull() @@ -26,6 +27,7 @@ class DatabaseClientsRepositoryTest { assertEquals(client.id, result?.id) assertEquals(client.ownerId, result?.ownerId) assertEquals(client.name, result?.name) + assertEquals(client.description, result?.description) assertEquals(client.secret, result?.secret) assertEquals(client.redirectUri, result?.redirectUri) } diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/services/jwt/JWTServiceTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/services/jwt/JWTServiceTest.kt index a74ae80..2cbdf12 100644 --- a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/services/jwt/JWTServiceTest.kt +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/services/jwt/JWTServiceTest.kt @@ -8,7 +8,7 @@ class JWTServiceTest { @Test fun testGenerateJWT() { - val service = JWTService("secret", "issuer", 1000L, 2000L) + val service = JWTService("secret", "issuer") val token = service.generateJWT("uid", "cid", "access") val verifier = JWT.require(Algorithm.HMAC256("secret")) .withIssuer("issuer") diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCaseTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCaseTest.kt index 1959dfe..4e386d5 100644 --- a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCaseTest.kt +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/CreateAuthCodeUseCaseTest.kt @@ -25,7 +25,7 @@ class CreateAuthCodeUseCaseTest { "code", useCase( ClientForUser( - Client("cid", "oid", "name", "secret", "redirect"), + Client("cid", "oid", "name", "description", "secret", "redirect"), User("uid", "displayName", "username") ) ) @@ -41,7 +41,7 @@ class CreateAuthCodeUseCaseTest { null, useCase( ClientForUser( - Client("cid", "oid", "name", "secret", "redirect"), + Client("cid", "oid", "name", "description", "secret", "redirect"), User("uid", "displayName", "username") ) ) diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCaseTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCaseTest.kt index 8b4526e..65f8ea0 100644 --- a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCaseTest.kt +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GenerateAuthTokenUseCaseTest.kt @@ -27,7 +27,7 @@ class GenerateAuthTokenUseCaseTest { ), userCase( ClientForUser( - Client("cid", "oid", "name", "secret", "redirect"), + Client("cid", "oid", "name", "description", "secret", "redirect"), User("uid", "displayName", "username") ) ) diff --git a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCaseTest.kt b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCaseTest.kt index 69d44cf..f3a27ac 100644 --- a/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCaseTest.kt +++ b/extopy-backend/src/jvmTest/kotlin/me/nathanfallet/extopy/usecases/auth/GetAuthCodeUseCaseTest.kt @@ -28,7 +28,7 @@ class GetAuthCodeUseCaseTest { val getUserUseCase = mockk>() val useCase = GetAuthCodeUseCase(repository, getClientUseCase, getUserUseCase) val clientForUser = ClientForUser( - Client("cid", "oid", "name", "secret", "redirect"), + Client("cid", "oid", "name", "description", "secret", "redirect"), User("uid", "displayName", "username") ) coEvery { repository.get("code") } returns ClientInUser("code", "uid", "cid", tomorrow) @@ -70,7 +70,7 @@ class GetAuthCodeUseCaseTest { val getUserUseCase = mockk>() val useCase = GetAuthCodeUseCase(repository, getClientUseCase, getUserUseCase) val clientForUser = ClientForUser( - Client("cid", "oid", "name", "secret", "redirect"), + Client("cid", "oid", "name", "description", "secret", "redirect"), User("uid", "displayName", "username") ) coEvery { repository.get("code") } returns ClientInUser("code", "uid", "cid", tomorrow) diff --git a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/application/Client.kt b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/application/Client.kt index ab93547..df7c4fd 100644 --- a/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/application/Client.kt +++ b/extopy-commons/src/commonMain/kotlin/me/nathanfallet/extopy/models/application/Client.kt @@ -9,6 +9,7 @@ data class Client( override val id: String, val ownerId: String, val name: String, + val description: String, val secret: String, override val redirectUri: String, ) : IClient, IModel {