192 lines
5.4 KiB
TypeScript
192 lines
5.4 KiB
TypeScript
import type { APIEvent } from "@solidjs/start/server";
|
|
import {
|
|
authenticateUser,
|
|
authenticateWithApple,
|
|
createUserWithPassword,
|
|
forgotPassword,
|
|
resetPassword,
|
|
refreshAccessToken,
|
|
revokeUserSessions,
|
|
} from "~/server/services/user.service";
|
|
import { verifyJWT } from "~/server/auth/jwt";
|
|
|
|
/**
|
|
* REST-style auth endpoints for mobile clients (Android/iOS).
|
|
*
|
|
* These wrap the tRPC service functions into a simple JSON API
|
|
* that OkHttp-based Android clients can call without tRPC client.
|
|
*
|
|
* POST /api/auth/login - Email/password login
|
|
* POST /api/auth/signup - Create account with password
|
|
* POST /api/auth/google - Google Sign-In token exchange
|
|
* POST /api/auth/refresh - Refresh access token
|
|
* POST /api/auth/logout - Revoke all sessions
|
|
* POST /api/auth/forgot-password - Request password reset
|
|
* POST /api/auth/reset-password - Reset password with token
|
|
*/
|
|
|
|
export async function POST(event: APIEvent) {
|
|
const action = event.params.action;
|
|
const body = await event.request.json().catch(() => ({}));
|
|
|
|
try {
|
|
switch (action) {
|
|
case "login": {
|
|
const { email, password } = body;
|
|
if (!email || !password) {
|
|
return new Response(
|
|
JSON.stringify({ message: "Email and password are required" }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
const result = await authenticateUser(email, password);
|
|
return Response.json({
|
|
id: result.user.id,
|
|
name: result.user.name ?? "",
|
|
email: result.user.email,
|
|
accessToken: result.accessToken,
|
|
sessionToken: result.sessionToken,
|
|
isNewUser: false,
|
|
});
|
|
}
|
|
|
|
case "signup": {
|
|
const { name, email, password } = body;
|
|
if (!email || !password) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
message: "Name, email, and password are required",
|
|
}),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
const result = await createUserWithPassword(
|
|
name ?? email.split("@")[0],
|
|
email,
|
|
password,
|
|
);
|
|
return Response.json({
|
|
id: result.user.id,
|
|
name: result.user.name ?? "",
|
|
email: result.user.email,
|
|
accessToken: result.accessToken,
|
|
sessionToken: result.sessionToken,
|
|
isNewUser: true,
|
|
});
|
|
}
|
|
|
|
case "apple": {
|
|
const { identityToken, authorizationCode, userIdentifier } = body;
|
|
if (!identityToken || !authorizationCode) {
|
|
return new Response(
|
|
JSON.stringify({
|
|
message: "identityToken and authorizationCode are required",
|
|
}),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
const result = await authenticateWithApple(
|
|
identityToken,
|
|
authorizationCode,
|
|
userIdentifier ?? null,
|
|
);
|
|
return Response.json({
|
|
id: result.user.id,
|
|
name: result.user.name ?? "",
|
|
email: result.user.email,
|
|
image: result.user.image,
|
|
accessToken: result.accessToken,
|
|
refreshToken: result.refreshToken,
|
|
sessionToken: result.sessionToken,
|
|
isNewUser: result.isNewUser ?? false,
|
|
});
|
|
}
|
|
|
|
case "refresh": {
|
|
const { refreshToken } = body;
|
|
if (!refreshToken) {
|
|
return new Response(
|
|
JSON.stringify({ message: "refreshToken is required" }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
const result = await refreshAccessToken(refreshToken);
|
|
return Response.json({
|
|
accessToken: result.accessToken,
|
|
refreshToken: result.refreshToken,
|
|
});
|
|
}
|
|
|
|
case "logout": {
|
|
// Extract user from Bearer token
|
|
const authHeader = event.request.headers.get("authorization");
|
|
if (authHeader?.startsWith("Bearer ")) {
|
|
const token = authHeader.slice(7);
|
|
try {
|
|
const payload = await verifyJWT<{ sub: string }>(token);
|
|
await revokeUserSessions(payload.sub);
|
|
} catch {
|
|
// Invalid token — still return success
|
|
}
|
|
}
|
|
return Response.json({ success: true });
|
|
}
|
|
|
|
case "forgot-password": {
|
|
const { email } = body;
|
|
if (!email) {
|
|
return new Response(
|
|
JSON.stringify({ message: "Email is required" }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
await forgotPassword(email);
|
|
return Response.json({ success: true });
|
|
}
|
|
|
|
case "reset-password": {
|
|
const { code, password } = body;
|
|
if (!code || !password) {
|
|
return new Response(
|
|
JSON.stringify({ message: "Code and password are required" }),
|
|
{ status: 400, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
// The mobile app sends "code" but the service expects "token"
|
|
// We accept both for backward compatibility
|
|
const token = code;
|
|
await resetPassword(token, password);
|
|
return Response.json({ success: true });
|
|
}
|
|
|
|
default:
|
|
return new Response(
|
|
JSON.stringify({ message: `Unknown action: ${action}` }),
|
|
{ status: 404, headers: { "Content-Type": "application/json" } },
|
|
);
|
|
}
|
|
} catch (error: any) {
|
|
const statusCode =
|
|
error.code === "UNAUTHORIZED"
|
|
? 401
|
|
: error.code === "CONFLICT"
|
|
? 409
|
|
: error.code === "NOT_FOUND"
|
|
? 404
|
|
: error.code === "FORBIDDEN"
|
|
? 403
|
|
: 500;
|
|
|
|
return new Response(
|
|
JSON.stringify({
|
|
message: error.message ?? "Internal server error",
|
|
code: error.code ?? "INTERNAL_ERROR",
|
|
}),
|
|
{
|
|
status: statusCode,
|
|
headers: { "Content-Type": "application/json" },
|
|
},
|
|
);
|
|
}
|
|
}
|