Skip to content

Commit

Permalink
refactor: new auth system reimplemented
Browse files Browse the repository at this point in the history
  • Loading branch information
nathanfallet committed Mar 12, 2024
1 parent 4e47443 commit 8f86b9c
Show file tree
Hide file tree
Showing 59 changed files with 516 additions and 556 deletions.
4 changes: 1 addition & 3 deletions extopy-backend/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ kotlin {
val ktorVersion = "2.3.9"
val koinVersion = "3.5.0"
val logbackVersion = "0.9.30"
val ktorxVersion = "2.2.4"
val ktorxVersion = "2.3.0"

sourceSets {
val commonMain by getting {
Expand Down Expand Up @@ -87,8 +87,6 @@ kotlin {
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-routers-auth:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-routers-auth-locale:$ktorxVersion")
implementation("me.nathanfallet.ktorx:ktor-sentry:$ktorxVersion")
implementation("me.nathanfallet.cloudflare:cloudflare-api-client:4.2.3")

Expand Down
Original file line number Diff line number Diff line change
@@ -1,90 +1,125 @@
package me.nathanfallet.extopy.controllers.auth

import io.ktor.http.*
import io.ktor.server.application.*
import me.nathanfallet.extopy.models.auth.LoginPayload
import me.nathanfallet.extopy.models.auth.RegisterCodePayload
import me.nathanfallet.extopy.models.auth.RegisterPayload
import me.nathanfallet.ktorx.controllers.auth.AbstractAuthWithCodeController
import me.nathanfallet.ktorx.models.annotations.*
import me.nathanfallet.ktorx.models.auth.ClientForUser
import me.nathanfallet.ktorx.usecases.auth.*
import me.nathanfallet.extopy.models.application.Client
import me.nathanfallet.extopy.models.application.Email
import me.nathanfallet.extopy.models.auth.*
import me.nathanfallet.extopy.models.users.User
import me.nathanfallet.extopy.usecases.application.ICreateCodeInEmailUseCase
import me.nathanfallet.extopy.usecases.application.IDeleteCodeInEmailUseCase
import me.nathanfallet.extopy.usecases.application.IGetCodeInEmailUseCase
import me.nathanfallet.extopy.usecases.auth.*
import me.nathanfallet.ktorx.models.exceptions.ControllerException
import me.nathanfallet.ktorx.models.responses.RedirectResponse
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase
import me.nathanfallet.ktorx.usecases.users.IRequireUserForCallUseCase
import me.nathanfallet.usecases.auth.AuthRequest
import me.nathanfallet.usecases.auth.AuthToken
import me.nathanfallet.usecases.emails.ISendEmailUseCase
import me.nathanfallet.usecases.localization.ITranslateUseCase
import me.nathanfallet.usecases.models.get.IGetModelSuspendUseCase

class AuthController(
loginUseCase: ILoginUseCase<LoginPayload>,
registerUseCase: IRegisterUseCase<RegisterCodePayload>,
createSessionForUserUseCase: ICreateSessionForUserUseCase,
setSessionForCallUseCase: ISetSessionForCallUseCase,
createCodeRegisterUseCase: ICreateCodeRegisterUseCase<RegisterPayload>,
getCodeRegisterUseCase: IGetCodeRegisterUseCase<RegisterPayload>,
deleteCodeRegisterUseCase: IDeleteCodeRegisterUseCase,
requireUserForCallUseCase: IRequireUserForCallUseCase,
getClientUseCase: IGetClientUseCase,
getAuthCodeUseCase: IGetAuthCodeUseCase,
createAuthCodeUseCase: ICreateAuthCodeUseCase,
deleteAuthCodeUseCase: IDeleteAuthCodeUseCase,
generateAuthTokenUseCase: IGenerateAuthTokenUseCase,
) : AbstractAuthWithCodeController<LoginPayload, RegisterPayload, RegisterCodePayload>(
loginUseCase,
registerUseCase,
createSessionForUserUseCase,
setSessionForCallUseCase,
createCodeRegisterUseCase,
getCodeRegisterUseCase,
deleteCodeRegisterUseCase,
requireUserForCallUseCase,
getClientUseCase,
getAuthCodeUseCase,
createAuthCodeUseCase,
deleteAuthCodeUseCase,
generateAuthTokenUseCase,
) {
private val loginUseCase: ILoginUseCase,
private val registerUseCase: IRegisterUseCase,
private val setSessionForCallUseCase: ISetSessionForCallUseCase,
private val clearSessionForCallUseCase: IClearSessionForCallUseCase,
private val requireUserForCallUseCase: IRequireUserForCallUseCase,
private val getClientUseCase: IGetModelSuspendUseCase<Client, String>,
private val getAuthCodeUseCase: IGetAuthCodeUseCase,
private val createAuthCodeUseCase: ICreateAuthCodeUseCase,
private val deleteAuthCodeUseCase: IDeleteAuthCodeUseCase,
private val generateAuthTokenUseCase: IGenerateAuthTokenUseCase,
private val createCodeInEmailUseCase: ICreateCodeInEmailUseCase,
private val getCodeInEmailUseCase: IGetCodeInEmailUseCase,
private val deleteCodeInEmailUseCase: IDeleteCodeInEmailUseCase,
private val sendEmailUseCase: ISendEmailUseCase,
private val getLocaleForCallUseCase: IGetLocaleForCallUseCase,
private val translateUseCase: ITranslateUseCase,
) : IAuthController {

@TemplateMapping("auth/login.ftl")
@LoginPath
override suspend fun login(call: ApplicationCall, @Payload payload: LoginPayload) {
super.login(call, payload)
override fun login() {}

override suspend fun login(call: ApplicationCall, payload: LoginPayload, redirect: String?): RedirectResponse {
val user = loginUseCase(payload) ?: throw ControllerException(
HttpStatusCode.Unauthorized, "auth_invalid_credentials"
)
setSessionForCallUseCase(call, SessionPayload(user.id))
return RedirectResponse(redirect ?: "/")
}

override suspend fun logout(call: ApplicationCall, redirect: String?): RedirectResponse {
clearSessionForCallUseCase(call)
return RedirectResponse(redirect ?: "/")
}

@TemplateMapping("auth/register.ftl")
@RegisterPath
override suspend fun register(call: ApplicationCall, @Payload payload: RegisterPayload) {
super.register(call, payload)
override fun register() {}

override suspend fun register(call: ApplicationCall, payload: RegisterPayload): Map<String, Any> {
val code = createCodeInEmailUseCase(payload.email) ?: throw ControllerException(
HttpStatusCode.BadRequest, "auth_register_email_taken"
)
val locale = getLocaleForCallUseCase(call)
sendEmailUseCase(
Email(
translateUseCase(locale, "auth_register_email_title"),
translateUseCase(locale, "auth_register_email_body", listOf(code.code))
),
listOf(payload.email)
)
return mapOf("success" to "auth_register_code_created")
}

@TemplateMapping("auth/register.ftl")
@RegisterCodePath
override suspend fun register(call: ApplicationCall, code: String): RegisterPayload {
return super.register(call, code)
override suspend fun registerCode(call: ApplicationCall, code: String): RegisterPayload {
val codeInEmail = getCodeInEmailUseCase(code) ?: throw ControllerException(
HttpStatusCode.NotFound, "auth_code_invalid"
)
return RegisterPayload(codeInEmail.email)
}

@TemplateMapping("auth/register.ftl")
@RegisterCodeRedirectPath
override suspend fun register(call: ApplicationCall, code: String, @Payload payload: RegisterCodePayload) {
super.register(call, code, payload)
override suspend fun registerCode(
call: ApplicationCall,
code: String,
payload: RegisterCodePayload,
redirect: String?,
): RedirectResponse {
val user = registerUseCase(code, payload) ?: throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
setSessionForCallUseCase(call, SessionPayload(user.id))
deleteCodeInEmailUseCase(code)
return RedirectResponse(redirect ?: "/")
}

@TemplateMapping("auth/authorize.ftl")
@AuthorizePath
override suspend fun authorize(call: ApplicationCall, clientId: String?): ClientForUser {
return super.authorize(call, clientId)
val user = requireUserForCallUseCase(call)
val client = clientId?.let { getClientUseCase(it) } ?: throw ControllerException(
HttpStatusCode.BadRequest, "auth_invalid_client"
)
return ClientForUser(client, user as User)
}

@TemplateMapping("auth/authorize.ftl")
@AuthorizeRedirectPath
override suspend fun authorize(call: ApplicationCall, client: ClientForUser): String {
return super.authorize(call, client)
override suspend fun authorizeRedirect(call: ApplicationCall, clientId: String?): Map<String, Any> {
val client = authorize(call, clientId)
val code = createAuthCodeUseCase(client) ?: throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
return mapOf(
"redirect" to client.client.redirectUri.replace("{code}", code)
)
}

@APIMapping
@CreateModelPath("/token")
@DocumentedTag("Auth")
@DocumentedError(400, "auth_invalid_code")
@DocumentedError(500, "error_internal")
override suspend fun token(call: ApplicationCall, @Payload request: AuthRequest): AuthToken {
return super.token(call, request)
override suspend fun token(payload: AuthRequest): AuthToken {
val client = getAuthCodeUseCase(payload.code)?.takeIf {
it.client.clientId == payload.clientId && it.client.clientSecret == payload.clientSecret
} ?: throw ControllerException(
HttpStatusCode.BadRequest, "auth_invalid_code"
)
if (!deleteAuthCodeUseCase(payload.code)) throw ControllerException(
HttpStatusCode.InternalServerError, "error_internal"
)
return generateAuthTokenUseCase(client)
}

}
Original file line number Diff line number Diff line change
@@ -1,36 +1,28 @@
package me.nathanfallet.extopy.controllers.auth

import io.ktor.server.freemarker.*
import io.ktor.util.reflect.*
import me.nathanfallet.extopy.models.auth.LoginPayload
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.routers.api.APIUnitRouter
import me.nathanfallet.ktorx.routers.auth.LocalizedAuthWithCodeTemplateRouter
import me.nathanfallet.ktorx.routers.concat.ConcatUnitRouter
import me.nathanfallet.ktorx.routers.templates.LocalizedTemplateUnitRouter
import me.nathanfallet.ktorx.usecases.localization.IGetLocaleForCallUseCase

class AuthRouter(
controller: IAuthWithCodeController<LoginPayload, RegisterPayload, RegisterCodePayload>,
controller: IAuthController,
getLocaleForCallUseCase: IGetLocaleForCallUseCase,
) : ConcatUnitRouter(
LocalizedAuthWithCodeTemplateRouter(
typeInfo<LoginPayload>(),
typeInfo<RegisterPayload>(),
typeInfo<RegisterCodePayload>(),
LocalizedTemplateUnitRouter(
controller,
AuthController::class,
IAuthController::class,
{ template, model -> respondTemplate(template, model) },
getLocaleForCallUseCase,
null,
"/auth/login?redirect={path}",
"auth/redirect.ftl",
errorTemplate = null,
redirectUnauthorizedToUrl = "/auth/login?redirect={path}",
route = "auth",
),
APIUnitRouter(
controller,
AuthController::class,
route = "/auth",
IAuthController::class,
route = "auth",
prefix = "/api/v1"
)
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package me.nathanfallet.extopy.controllers.auth

import io.ktor.server.application.*
import me.nathanfallet.extopy.models.auth.ClientForUser
import me.nathanfallet.extopy.models.auth.LoginPayload
import me.nathanfallet.extopy.models.auth.RegisterCodePayload
import me.nathanfallet.extopy.models.auth.RegisterPayload
import me.nathanfallet.ktorx.controllers.IUnitController
import me.nathanfallet.ktorx.models.annotations.*
import me.nathanfallet.ktorx.models.responses.RedirectResponse
import me.nathanfallet.usecases.auth.AuthRequest
import me.nathanfallet.usecases.auth.AuthToken

interface IAuthController : IUnitController {

@TemplateMapping("auth/login.ftl")
@Path("GET", "/login")
fun login()

@TemplateMapping("auth/login.ftl")
@Path("POST", "/login")
suspend fun login(
call: ApplicationCall,
@Payload payload: LoginPayload,
@QueryParameter redirect: String?,
): RedirectResponse

@TemplateMapping("auth/login.ftl")
@Path("GET", "/logout")
suspend fun logout(call: ApplicationCall, @QueryParameter redirect: String?): RedirectResponse

@TemplateMapping("auth/register.ftl")
@Path("GET", "/register")
fun register()

@TemplateMapping("auth/register.ftl")
@Path("POST", "/register")
suspend fun register(call: ApplicationCall, @Payload payload: RegisterPayload): Map<String, Any>

@TemplateMapping("auth/register.ftl")
@Path("GET", "/register/{code}")
suspend fun registerCode(call: ApplicationCall, @PathParameter code: String): RegisterPayload

@TemplateMapping("auth/register.ftl")
@Path("POST", "/register/{code}")
suspend fun registerCode(
call: ApplicationCall,
@PathParameter code: String,
@Payload payload: RegisterCodePayload,
@QueryParameter redirect: String?,
): RedirectResponse

@TemplateMapping("auth/authorize.ftl")
@Path("GET", "/authorize")
suspend fun authorize(call: ApplicationCall, @QueryParameter clientId: String?): ClientForUser

@TemplateMapping("auth/redirect.ftl")
@Path("POST", "/authorize")
suspend fun authorizeRedirect(call: ApplicationCall, @QueryParameter clientId: String?): Map<String, Any>

@APIMapping
@Path("POST", "/token")
@DocumentedTag("Auth")
@DocumentedError(400, "auth_invalid_code")
@DocumentedError(500, "error_internal")
suspend fun token(@Payload payload: AuthRequest): AuthToken

}
Loading

0 comments on commit 8f86b9c

Please sign in to comment.