274 lines
6.7 KiB
JavaScript
274 lines
6.7 KiB
JavaScript
"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,
|
|
};
|