-
-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
refactor: new auth system reimplemented
- Loading branch information
1 parent
4e47443
commit 8f86b9c
Showing
59 changed files
with
516 additions
and
556 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
165 changes: 100 additions & 65 deletions
165
...y-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
} | ||
|
||
} |
26 changes: 9 additions & 17 deletions
26
extopy-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/AuthRouter.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
) | ||
) |
68 changes: 68 additions & 0 deletions
68
...-backend/src/commonMain/kotlin/me/nathanfallet/extopy/controllers/auth/IAuthController.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
} |
Oops, something went wrong.