feat: migrate auth and state flows to better-auth and convex
This commit is contained in:
273
src/integrations/better-auth.js
Normal file
273
src/integrations/better-auth.js
Normal file
@@ -0,0 +1,273 @@
|
||||
"use strict";
|
||||
|
||||
function sanitizeUserId(userId) {
|
||||
return String(userId || "")
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9._-]/g, "-")
|
||||
.replace(/-+/g, "-")
|
||||
.replace(/^-|-$/g, "");
|
||||
}
|
||||
|
||||
function resolveEmailFromUserId(userId) {
|
||||
const raw = String(userId || "").trim();
|
||||
if (raw.includes("@")) {
|
||||
return raw.toLowerCase();
|
||||
}
|
||||
|
||||
const safe = sanitizeUserId(raw) || "user";
|
||||
return `${safe}@xartaudio.local`;
|
||||
}
|
||||
|
||||
function extractLegacyUserCookie(cookieHeader) {
|
||||
const match = String(cookieHeader || "").match(/(?:^|;\s*)xartaudio_user=([^;]+)/);
|
||||
if (!match) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
return decodeURIComponent(match[1]);
|
||||
} catch {
|
||||
return match[1];
|
||||
}
|
||||
}
|
||||
|
||||
function headersFromObject(rawHeaders = {}) {
|
||||
const headers = new Headers();
|
||||
for (const [key, value] of Object.entries(rawHeaders)) {
|
||||
if (value === undefined || value === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) {
|
||||
headers.append(key, String(item));
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
headers.set(key, String(value));
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function responseHeadersToObject(responseHeaders) {
|
||||
const headers = {};
|
||||
for (const [key, value] of responseHeaders.entries()) {
|
||||
headers[key] = value;
|
||||
}
|
||||
|
||||
if (typeof responseHeaders.getSetCookie === "function") {
|
||||
const cookies = responseHeaders.getSetCookie();
|
||||
if (cookies && cookies.length > 0) {
|
||||
headers["set-cookie"] = cookies.length === 1 ? cookies[0] : cookies;
|
||||
}
|
||||
} else {
|
||||
const cookie = responseHeaders.get("set-cookie");
|
||||
if (cookie) {
|
||||
headers["set-cookie"] = cookie;
|
||||
}
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
function createBetterAuthAdapter({
|
||||
appBaseUrl,
|
||||
basePath = "/api/auth",
|
||||
secret,
|
||||
devPassword = "xartaudio-dev-password",
|
||||
authHandler,
|
||||
logger = console,
|
||||
} = {}) {
|
||||
const normalizedBasePath = basePath.startsWith("/") ? basePath : `/${basePath}`;
|
||||
const memoryDb = {
|
||||
user: [],
|
||||
session: [],
|
||||
account: [],
|
||||
verification: [],
|
||||
};
|
||||
|
||||
let handlerPromise = null;
|
||||
|
||||
async function resolveHandler() {
|
||||
if (authHandler) {
|
||||
return authHandler;
|
||||
}
|
||||
|
||||
if (!handlerPromise) {
|
||||
handlerPromise = (async () => {
|
||||
const [{ betterAuth }, { memoryAdapter }] = await Promise.all([
|
||||
import("better-auth"),
|
||||
import("better-auth/adapters/memory"),
|
||||
]);
|
||||
|
||||
const auth = betterAuth({
|
||||
appName: "XArtAudio",
|
||||
baseURL: appBaseUrl,
|
||||
basePath: normalizedBasePath,
|
||||
secret,
|
||||
trustedOrigins: [appBaseUrl],
|
||||
database: memoryAdapter(memoryDb),
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
autoSignIn: true,
|
||||
requireEmailVerification: false,
|
||||
minPasswordLength: 8,
|
||||
},
|
||||
});
|
||||
|
||||
return auth.handler;
|
||||
})().catch((error) => {
|
||||
logger.error({ err: error }, "failed to initialize better-auth");
|
||||
handlerPromise = null;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
return handlerPromise;
|
||||
}
|
||||
|
||||
async function invoke({ method, path, headers, rawBody = "" }) {
|
||||
const handler = await resolveHandler();
|
||||
const requestHeaders = headersFromObject(headers || {});
|
||||
const url = new URL(path, appBaseUrl).toString();
|
||||
const body = method === "GET" || method === "HEAD" ? undefined : rawBody;
|
||||
|
||||
const response = await handler(new Request(url, {
|
||||
method,
|
||||
headers: requestHeaders,
|
||||
body,
|
||||
}));
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
return {
|
||||
isConfigured() {
|
||||
return Boolean(appBaseUrl && secret);
|
||||
},
|
||||
|
||||
handlesPath(path) {
|
||||
return path === normalizedBasePath || path.startsWith(`${normalizedBasePath}/`);
|
||||
},
|
||||
|
||||
async handleRoute({ method, path, headers, rawBody }) {
|
||||
const response = await invoke({ method, path, headers, rawBody });
|
||||
const responseBody = await response.text();
|
||||
|
||||
return {
|
||||
status: response.status,
|
||||
headers: responseHeadersToObject(response.headers),
|
||||
body: responseBody,
|
||||
};
|
||||
},
|
||||
|
||||
async getAuthenticatedUserId(headers = {}) {
|
||||
if (headers["x-user-id"]) {
|
||||
return String(headers["x-user-id"]);
|
||||
}
|
||||
|
||||
const cookieHeader = headers.cookie || "";
|
||||
if (!cookieHeader) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await invoke({
|
||||
method: "GET",
|
||||
path: `${normalizedBasePath}/get-session`,
|
||||
headers: {
|
||||
cookie: cookieHeader,
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = await response.json().catch(() => null);
|
||||
const user = payload && payload.user ? payload.user : null;
|
||||
if (!user) {
|
||||
return extractLegacyUserCookie(cookieHeader);
|
||||
}
|
||||
|
||||
return user.name || user.email || user.id || null;
|
||||
} catch {
|
||||
return extractLegacyUserCookie(cookieHeader);
|
||||
}
|
||||
},
|
||||
|
||||
async signInDevUser(userId) {
|
||||
const email = resolveEmailFromUserId(userId);
|
||||
const signInBody = JSON.stringify({
|
||||
email,
|
||||
password: devPassword,
|
||||
rememberMe: true,
|
||||
});
|
||||
|
||||
let response = await invoke({
|
||||
method: "POST",
|
||||
path: `${normalizedBasePath}/sign-in/email`,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
},
|
||||
rawBody: signInBody,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
response = await invoke({
|
||||
method: "POST",
|
||||
path: `${normalizedBasePath}/sign-up/email`,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
accept: "application/json",
|
||||
},
|
||||
rawBody: JSON.stringify({
|
||||
name: String(userId),
|
||||
email,
|
||||
password: devPassword,
|
||||
rememberMe: true,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const details = await response.text().catch(() => "");
|
||||
throw new Error(`auth_sign_in_failed:${response.status}:${details}`);
|
||||
}
|
||||
|
||||
const responseHeaders = responseHeadersToObject(response.headers);
|
||||
if (!responseHeaders["set-cookie"]) {
|
||||
throw new Error("auth_set_cookie_missing");
|
||||
}
|
||||
|
||||
return {
|
||||
setCookie: responseHeaders["set-cookie"],
|
||||
};
|
||||
},
|
||||
|
||||
async signOut(headers = {}) {
|
||||
const response = await invoke({
|
||||
method: "POST",
|
||||
path: `${normalizedBasePath}/sign-out`,
|
||||
headers: {
|
||||
cookie: headers.cookie || "",
|
||||
accept: "application/json",
|
||||
},
|
||||
});
|
||||
|
||||
const responseHeaders = responseHeadersToObject(response.headers);
|
||||
return {
|
||||
ok: response.ok,
|
||||
setCookie: responseHeaders["set-cookie"] || null,
|
||||
};
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
createBetterAuthAdapter,
|
||||
};
|
||||
Reference in New Issue
Block a user