From e5aab01b85fa866727ad5990cfbf256078ea17ef Mon Sep 17 00:00:00 2001 From: Priveetee Date: Thu, 11 Jun 2026 10:19:08 +0200 Subject: [PATCH] fix: expose registration status endpoint --- .../typetype/server/routes/AuthRouteModels.kt | 3 ++ .../dev/typetype/server/routes/AuthRoutes.kt | 22 +------- .../typetype/server/routes/RegisterRoutes.kt | 52 +++++++++++++++++++ .../server/RegistrationSettingsTest.kt | 28 ++++++++++ 4 files changed, 85 insertions(+), 20 deletions(-) create mode 100644 src/main/kotlin/dev/typetype/server/routes/RegisterRoutes.kt diff --git a/src/main/kotlin/dev/typetype/server/routes/AuthRouteModels.kt b/src/main/kotlin/dev/typetype/server/routes/AuthRouteModels.kt index 65767c5c..13b469c9 100644 --- a/src/main/kotlin/dev/typetype/server/routes/AuthRouteModels.kt +++ b/src/main/kotlin/dev/typetype/server/routes/AuthRouteModels.kt @@ -3,6 +3,9 @@ package dev.typetype.server.routes @kotlinx.serialization.Serializable data class RegisterRequest(val email: String, val password: String, val name: String) +@kotlinx.serialization.Serializable +data class RegisterStatusResponse(val allowRegistration: Boolean, val bootstrapAvailable: Boolean) + @kotlinx.serialization.Serializable data class LoginRequest(val identifier: String? = null, val email: String? = null, val password: String) diff --git a/src/main/kotlin/dev/typetype/server/routes/AuthRoutes.kt b/src/main/kotlin/dev/typetype/server/routes/AuthRoutes.kt index 1fbf2f7a..0fed3498 100644 --- a/src/main/kotlin/dev/typetype/server/routes/AuthRoutes.kt +++ b/src/main/kotlin/dev/typetype/server/routes/AuthRoutes.kt @@ -18,26 +18,8 @@ import io.ktor.server.routing.get import io.ktor.server.routing.post fun Route.authRoutes(authService: AuthService, passwordResetService: PasswordResetService, profileService: ProfileService, adminSettingsService: AdminSettingsService, warmupService: HomeRecommendationWarmup = NoopHomeRecommendationWarmup) { - post("/auth/register") { - val req = call.receive() - if (req.email.isBlank() || req.password.isBlank() || req.name.isBlank()) { - call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing fields")) - return@post - } - val registrationAllowed = adminSettingsService.get().allowRegistration || !authService.hasUsers() - if (!registrationAllowed) { - call.respond(HttpStatusCode.Forbidden, ErrorResponse("Registration is disabled")) - return@post - } - try { - val token = authService.register(req.email, req.password, req.name) - token.accessToken.warm(authService, warmupService) - AuthCookieHelpers.setRefreshCookie(call.response, token.refreshToken) - call.respond(SessionResponse(token.accessToken)) - } catch (e: Exception) { - call.respond(HttpStatusCode.BadRequest, ErrorResponse("Registration failed")) - } - } + registerRoutes(authService, adminSettingsService, warmupService) + post("/auth/login") { val req = call.receive() val identifier = req.identifier?.trim().orEmpty().ifBlank { req.email?.trim().orEmpty() } diff --git a/src/main/kotlin/dev/typetype/server/routes/RegisterRoutes.kt b/src/main/kotlin/dev/typetype/server/routes/RegisterRoutes.kt new file mode 100644 index 00000000..062680c1 --- /dev/null +++ b/src/main/kotlin/dev/typetype/server/routes/RegisterRoutes.kt @@ -0,0 +1,52 @@ +package dev.typetype.server.routes + +import dev.typetype.server.models.ErrorResponse +import dev.typetype.server.services.AdminSettingsService +import dev.typetype.server.services.AuthCookieHelpers +import dev.typetype.server.services.AuthService +import dev.typetype.server.services.HomeRecommendationWarmup +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.call +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.get +import io.ktor.server.routing.post + +fun Route.registerRoutes( + authService: AuthService, + adminSettingsService: AdminSettingsService, + warmupService: HomeRecommendationWarmup, +): Unit { + get("/auth/register/status") { + val bootstrapAvailable = !authService.hasUsers() + call.respond( + RegisterStatusResponse( + allowRegistration = adminSettingsService.get().allowRegistration, + bootstrapAvailable = bootstrapAvailable, + ) + ) + } + + post("/auth/register") { + val req = call.receive() + if (req.email.isBlank() || req.password.isBlank() || req.name.isBlank()) { + call.respond(HttpStatusCode.BadRequest, ErrorResponse("Missing fields")) + return@post + } + val bootstrapAvailable = !authService.hasUsers() + val registrationAllowed = adminSettingsService.get().allowRegistration || bootstrapAvailable + if (!registrationAllowed) { + call.respond(HttpStatusCode.Forbidden, ErrorResponse("Registration is disabled")) + return@post + } + try { + val token = authService.register(req.email, req.password, req.name) + authService.verify(token.accessToken)?.let(warmupService::markActive) + AuthCookieHelpers.setRefreshCookie(call.response, token.refreshToken) + call.respond(SessionResponse(token.accessToken)) + } catch (e: Exception) { + call.respond(HttpStatusCode.BadRequest, ErrorResponse("Registration failed")) + } + } +} diff --git a/src/test/kotlin/dev/typetype/server/RegistrationSettingsTest.kt b/src/test/kotlin/dev/typetype/server/RegistrationSettingsTest.kt index 8ce214de..71afd694 100644 --- a/src/test/kotlin/dev/typetype/server/RegistrationSettingsTest.kt +++ b/src/test/kotlin/dev/typetype/server/RegistrationSettingsTest.kt @@ -6,8 +6,10 @@ import dev.typetype.server.services.AdminSettingsService import dev.typetype.server.services.AuthService import dev.typetype.server.services.PasswordResetService import dev.typetype.server.services.ProfileService +import io.ktor.client.request.get import io.ktor.client.request.post import io.ktor.client.request.setBody +import io.ktor.client.statement.bodyAsText import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode import io.ktor.http.contentType @@ -66,4 +68,30 @@ class RegistrationSettingsTest { } assertEquals(HttpStatusCode.OK, response.status) } + + @Test + fun `register status exposes bootstrap availability`() = testApplication { + adminSettings.upsert(AdminSettingsItem(allowRegistration = false, allowGuest = true, forceEmailVerification = false)) + val auth = AuthService.fixed(TEST_USER_ID, hasUsers = false) + application { + install(ContentNegotiation) { json() } + routing { authRoutes(auth, passwordReset, profile, adminSettings) } + } + val response = client.get("/auth/register/status") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("""{"allowRegistration":false,"bootstrapAvailable":true}""", response.bodyAsText()) + } + + @Test + fun `register status closes when disabled and users exist`() = testApplication { + adminSettings.upsert(AdminSettingsItem(allowRegistration = false, allowGuest = true, forceEmailVerification = false)) + val auth = AuthService.fixed(TEST_USER_ID, hasUsers = true) + application { + install(ContentNegotiation) { json() } + routing { authRoutes(auth, passwordReset, profile, adminSettings) } + } + val response = client.get("/auth/register/status") + assertEquals(HttpStatusCode.OK, response.status) + assertEquals("""{"allowRegistration":false,"bootstrapAvailable":false}""", response.bodyAsText()) + } }