Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/kotlin/dev/typetype/server/routes/AuthRouteModels.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
22 changes: 2 additions & 20 deletions src/main/kotlin/dev/typetype/server/routes/AuthRoutes.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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<RegisterRequest>()
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<LoginRequest>()
val identifier = req.identifier?.trim().orEmpty().ifBlank { req.email?.trim().orEmpty() }
Expand Down
52 changes: 52 additions & 0 deletions src/main/kotlin/dev/typetype/server/routes/RegisterRoutes.kt
Original file line number Diff line number Diff line change
@@ -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<RegisterRequest>()
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"))
}
}
}
28 changes: 28 additions & 0 deletions src/test/kotlin/dev/typetype/server/RegistrationSettingsTest.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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())
}
}