feat: Implement email/password and X OAuth authentication, replacing the dev-login mechanism.

This commit is contained in:
Codex
2026-02-18 14:54:28 +00:00
parent c92032eb72
commit 76f991e690
15 changed files with 410 additions and 147 deletions

View File

@@ -5,6 +5,63 @@ const assert = require("node:assert/strict");
const { buildApp } = require("../src/app");
const { hmacSHA256Hex } = require("../src/lib/signature");
function getTestCookieValue(cookieHeader, name) {
const parts = String(cookieHeader || "").split(";").map((part) => part.trim());
const prefix = `${name}=`;
const valuePart = parts.find((part) => part.startsWith(prefix));
if (!valuePart) {
return null;
}
try {
return decodeURIComponent(valuePart.slice(prefix.length));
} catch {
return valuePart.slice(prefix.length);
}
}
function createTestAuthAdapter() {
return {
isConfigured() {
return true;
},
isXOAuthConfigured() {
return true;
},
handlesPath() {
return false;
},
async handleRoute() {
return { status: 404, headers: {}, body: "not_found" };
},
async getAuthenticatedUserId(headers = {}) {
return getTestCookieValue(headers.cookie, "xartaudio_user");
},
async signInWithEmail({ email }) {
return {
setCookie: `xartaudio_user=${encodeURIComponent(String(email))}; Path=/; HttpOnly`,
};
},
async signUpWithEmail({ email }) {
return {
setCookie: `xartaudio_user=${encodeURIComponent(String(email))}; Path=/; HttpOnly`,
};
},
async getXAuthorizationUrl() {
return {
url: "https://x.com/i/oauth2/authorize?state=test",
setCookie: "xartaudio_oauth_state=test; Path=/; HttpOnly",
};
},
async signOut() {
return {
ok: true,
setCookie: "xartaudio_user=; Path=/; Max-Age=0",
};
},
};
}
function createApp(options = {}) {
const baseConfig = {
xWebhookSecret: "x-secret",
@@ -17,7 +74,8 @@ function createApp(options = {}) {
appBaseUrl: "http://localhost:3000",
betterAuthSecret: "test-better-auth-secret",
betterAuthBasePath: "/api/auth",
betterAuthDevPassword: "xartaudio-dev-password",
xOAuthClientId: "x-client-id",
xOAuthClientSecret: "x-client-secret",
internalApiToken: "",
convexDeploymentUrl: "",
convexAuthToken: "",
@@ -75,6 +133,9 @@ function createApp(options = {}) {
const appOptions = { ...options };
delete appOptions.config;
if (!appOptions.authAdapter) {
appOptions.authAdapter = createTestAuthAdapter();
}
return buildApp({
config: mergedConfig,
@@ -108,7 +169,7 @@ test("GET / renders landing page", async () => {
const app = createApp();
const response = await call(app, { method: "GET", path: "/" });
assert.equal(response.status, 200);
assert.match(response.body, /From X Article to audiobook in one mention/);
assert.match(response.body, /From X Article to audiobook/);
});
test("GET /assets/styles.css serves compiled stylesheet", async () => {
@@ -127,17 +188,17 @@ test("unauthenticated /app redirects to /login with returnTo", async () => {
assert.match(response.headers.location, /returnTo=%2Fapp/);
});
test("POST /auth/dev-login sets cookie and redirects", async () => {
test("POST /auth/email/sign-in sets cookie and redirects", async () => {
const app = createApp();
const response = await call(app, {
method: "POST",
path: "/auth/dev-login",
body: "userId=matiss&returnTo=%2Fapp",
path: "/auth/email/sign-in",
body: "email=matiss%40example.com&password=password123&returnTo=%2Fapp",
});
assert.equal(response.status, 303);
assert.equal(response.headers.location, "/app");
assert.match(String(response.headers["set-cookie"]), /HttpOnly/);
assert.match(String(response.headers["set-cookie"]), /xartaudio_user=matiss%40example\.com/);
});
test("authenticated dashboard topup + simulate mention flow", async () => {
@@ -954,15 +1015,15 @@ test("rate limits repeated login attempts from same IP", async () => {
const first = await call(app, {
method: "POST",
path: "/auth/dev-login",
path: "/auth/email/sign-in",
headers: { "x-forwarded-for": "5.5.5.5" },
body: "userId=alice&returnTo=%2Fapp",
body: "email=alice%40example.com&password=password123&returnTo=%2Fapp",
});
const second = await call(app, {
method: "POST",
path: "/auth/dev-login",
path: "/auth/email/sign-in",
headers: { "x-forwarded-for": "5.5.5.5" },
body: "userId=alice&returnTo=%2Fapp",
body: "email=alice%40example.com&password=password123&returnTo=%2Fapp",
});
assert.equal(first.status, 303);