253 lines
5.8 KiB
TypeScript
253 lines
5.8 KiB
TypeScript
import {
|
|
auth,
|
|
authRouteHandlers,
|
|
canDeleteUserAccount,
|
|
canUserSelfRegister,
|
|
ensureSupportUserBootstrap,
|
|
ensureUserUsername,
|
|
hasOwnerUser,
|
|
promoteFirstRegisteredUserToOwner,
|
|
resolveEmailFromLoginIdentifier,
|
|
} from "@/lib/auth/server"
|
|
|
|
export const runtime = "nodejs"
|
|
|
|
type AuthPostResponse = {
|
|
user?: {
|
|
id?: string
|
|
role?: string
|
|
email?: string
|
|
name?: string
|
|
username?: string
|
|
}
|
|
message?: string
|
|
}
|
|
|
|
function jsonResponse(payload: unknown, status: number): Response {
|
|
return Response.json(payload, { status })
|
|
}
|
|
|
|
async function parseJsonBody(request: Request): Promise<Record<string, unknown> | null> {
|
|
return (await request.json().catch(() => null)) as Record<string, unknown> | null
|
|
}
|
|
|
|
function buildJsonRequest(request: Request, body: Record<string, unknown>): Request {
|
|
const headers = new Headers(request.headers)
|
|
headers.set("content-type", "application/json")
|
|
|
|
return new Request(request.url, {
|
|
method: request.method,
|
|
headers,
|
|
body: JSON.stringify(body),
|
|
})
|
|
}
|
|
|
|
function isDeleteUserAuthPath(pathname: string): boolean {
|
|
const actionPrefix = "/api/auth/"
|
|
const actionIndex = pathname.indexOf(actionPrefix)
|
|
|
|
if (actionIndex === -1) {
|
|
return false
|
|
}
|
|
|
|
const actionPath = pathname.slice(actionIndex + actionPrefix.length)
|
|
return actionPath === "delete-user" || actionPath.startsWith("delete-user/")
|
|
}
|
|
|
|
async function guardProtectedAccountDeletion(request: Request): Promise<Response | null> {
|
|
const pathname = new URL(request.url).pathname
|
|
|
|
if (!isDeleteUserAuthPath(pathname)) {
|
|
return null
|
|
}
|
|
|
|
const session = await auth.api
|
|
.getSession({
|
|
headers: request.headers,
|
|
})
|
|
.catch(() => null)
|
|
|
|
const userId = session?.user?.id
|
|
|
|
if (!userId) {
|
|
return null
|
|
}
|
|
|
|
const allowed = await canDeleteUserAccount(userId)
|
|
|
|
if (allowed) {
|
|
return null
|
|
}
|
|
|
|
return jsonResponse(
|
|
{
|
|
message: "This account is protected and cannot be deleted.",
|
|
},
|
|
403,
|
|
)
|
|
}
|
|
|
|
async function handleSignInPost(request: Request): Promise<Response> {
|
|
await ensureSupportUserBootstrap()
|
|
|
|
const body = await parseJsonBody(request)
|
|
const identifier = typeof body?.identifier === "string" ? body.identifier : null
|
|
const rawEmail = typeof body?.email === "string" ? body.email : null
|
|
const resolvedEmail = await resolveEmailFromLoginIdentifier(identifier ?? rawEmail)
|
|
|
|
if (!resolvedEmail) {
|
|
return jsonResponse(
|
|
{
|
|
message: "Invalid email or username.",
|
|
},
|
|
401,
|
|
)
|
|
}
|
|
|
|
const rewrittenBody = {
|
|
...(body ?? {}),
|
|
email: resolvedEmail,
|
|
}
|
|
|
|
return authRouteHandlers.POST(buildJsonRequest(request, rewrittenBody))
|
|
}
|
|
|
|
async function handleSignUpPost(request: Request): Promise<Response> {
|
|
await ensureSupportUserBootstrap()
|
|
|
|
const signUpBody = await parseJsonBody(request)
|
|
const preferredUsername =
|
|
typeof signUpBody?.username === "string" ? signUpBody.username : undefined
|
|
const { username: _ignoredUsername, ...signUpBodyWithoutUsername } = signUpBody ?? {}
|
|
|
|
const hadOwnerBeforeSignUp = await hasOwnerUser()
|
|
const registrationEnabled = await canUserSelfRegister()
|
|
|
|
if (!registrationEnabled) {
|
|
return jsonResponse(
|
|
{
|
|
message: "Registration is currently disabled.",
|
|
},
|
|
403,
|
|
)
|
|
}
|
|
|
|
const response = await authRouteHandlers.POST(
|
|
buildJsonRequest(request, {
|
|
...signUpBodyWithoutUsername,
|
|
}),
|
|
)
|
|
|
|
if (!response.ok) {
|
|
return response
|
|
}
|
|
|
|
const payload = (await response
|
|
.clone()
|
|
.json()
|
|
.catch(() => null)) as AuthPostResponse | null
|
|
const userId = payload?.user?.id
|
|
|
|
if (!userId) {
|
|
return response
|
|
}
|
|
|
|
await ensureUserUsername(userId, {
|
|
preferred: preferredUsername,
|
|
fallbackEmail: payload?.user?.email,
|
|
fallbackName: payload?.user?.name,
|
|
})
|
|
|
|
if (hadOwnerBeforeSignUp || !payload?.user) {
|
|
return response
|
|
}
|
|
|
|
const promoted = await promoteFirstRegisteredUserToOwner(userId)
|
|
|
|
if (!promoted) {
|
|
return jsonResponse(
|
|
{
|
|
message: "Initial owner registration window has just closed. Please sign in instead.",
|
|
},
|
|
409,
|
|
)
|
|
}
|
|
|
|
payload.user.role = "owner"
|
|
|
|
return new Response(JSON.stringify(payload), {
|
|
status: response.status,
|
|
headers: response.headers,
|
|
})
|
|
}
|
|
|
|
export async function GET(request: Request): Promise<Response> {
|
|
await ensureSupportUserBootstrap()
|
|
|
|
const deletionGuardResponse = await guardProtectedAccountDeletion(request)
|
|
|
|
if (deletionGuardResponse) {
|
|
return deletionGuardResponse
|
|
}
|
|
|
|
return authRouteHandlers.GET(request)
|
|
}
|
|
|
|
export async function POST(request: Request): Promise<Response> {
|
|
const pathname = new URL(request.url).pathname
|
|
|
|
if (pathname.endsWith("/sign-in/email")) {
|
|
return handleSignInPost(request)
|
|
}
|
|
|
|
if (pathname.endsWith("/sign-up/email")) {
|
|
return handleSignUpPost(request)
|
|
}
|
|
|
|
await ensureSupportUserBootstrap()
|
|
|
|
const deletionGuardResponse = await guardProtectedAccountDeletion(request)
|
|
|
|
if (deletionGuardResponse) {
|
|
return deletionGuardResponse
|
|
}
|
|
|
|
return authRouteHandlers.POST(request)
|
|
}
|
|
|
|
export async function PATCH(request: Request): Promise<Response> {
|
|
await ensureSupportUserBootstrap()
|
|
|
|
const deletionGuardResponse = await guardProtectedAccountDeletion(request)
|
|
|
|
if (deletionGuardResponse) {
|
|
return deletionGuardResponse
|
|
}
|
|
|
|
return authRouteHandlers.PATCH(request)
|
|
}
|
|
|
|
export async function PUT(request: Request): Promise<Response> {
|
|
await ensureSupportUserBootstrap()
|
|
|
|
const deletionGuardResponse = await guardProtectedAccountDeletion(request)
|
|
|
|
if (deletionGuardResponse) {
|
|
return deletionGuardResponse
|
|
}
|
|
|
|
return authRouteHandlers.PUT(request)
|
|
}
|
|
|
|
export async function DELETE(request: Request): Promise<Response> {
|
|
await ensureSupportUserBootstrap()
|
|
|
|
const deletionGuardResponse = await guardProtectedAccountDeletion(request)
|
|
|
|
if (deletionGuardResponse) {
|
|
return deletionGuardResponse
|
|
}
|
|
|
|
return authRouteHandlers.DELETE(request)
|
|
}
|