Complete Better Auth migration, harden security, update all docs

Console:
- Rewrite auth from JWT decoding to Better Auth useSession()
- Add auth-client.ts with createAuthClient
- Remove elydora_token localStorage, dead auth API functions
- Fix login/register pages to use signIn.email/signUp.email

Server:
- Upgrade better-auth ^1.2.0 → ^1.5.5
- CORS: enforce allowlist on all environments
- Sync auth service and middleware with backend

SDKs:
- Deprecate login/register in Node, Python, Go SDKs
- Update READMEs to show direct token initialization

Config:
- Sanitize .env with dev-only placeholder values
- Update helm chart and install scripts

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZacharyZhang-NYandClaude Opus 4.6 committed 2026-03-25 00:48:57 -04:00
1 parent e8fa5e557e
commit 27a8f0667c
39 files changed
+1464 -643

No files matched your search

+144 -33
View File
@@ -2,15 +2,17 @@
* Auth routes — registration, login, profile retrieval, and token refresh.
*
* /register and /login are public (no auth required).
* /me and /refresh require a valid JWT.
* /me, /refresh, and /token require a valid Better Auth session.
*
* Uses Better Auth for session management while preserving the existing
* API surface (endpoint paths and response formats).
*/
import { Hono } from 'hono';
import type { Env, AppVariables } from '../types.js';
import { authMiddleware } from '../middleware/auth.js';
import { registerUser, loginUser, getMe, issueJWT, issueApiToken } from '../services/auth-service.js';
import type { RbacRole } from '../shared/index.js';
import { DEFAULT_JWT_TTL_SECONDS } from '../shared/index.js';
import { getMe } from '../services/auth-service.js';
import { createAuth } from '../lib/auth.js';
import { getMessage } from '../i18n/messages.js';
const auth = new Hono<{ Bindings: Env; Variables: AppVariables }>();
@@ -22,28 +24,111 @@ auth.post('/register', async (c) => {
const body = await c.req.json();
const { email, password, display_name, org_name } = body;
const result = await registerUser(
c.env.ELYDORA_DB,
c.env.JWT_SECRET,
email,
password,
display_name,
org_name,
const betterAuthInstance = createAuth(
process.env.DATABASE_URL!,
c.env.BETTER_AUTH_SECRET,
c.env.BETTER_AUTH_URL,
c.env.ALLOWED_ORIGINS,
);
return c.json(result, 201);
// Use Better Auth to sign up the user
const signUpResult = await betterAuthInstance.api.signUpEmail({
body: {
email,
password,
name: display_name || email.split('@')[0]!,
},
});
if (!signUpResult) {
const lang = c.get('lang') ?? 'en';
return c.json({ error: { code: 'VALIDATION_ERROR', message: getMessage('auth.emailAlreadyRegistered', lang), request_id: '' } }, 409);
}
const user = signUpResult.user as { id: string; email: string; name: string; org_id?: string; role?: string; status?: string; createdAt?: unknown; updatedAt?: unknown };
// Create the organization via the database adapter (preserving existing schema)
const now = Date.now();
const { generateUUIDv7 } = await import('../utils/uuid.js');
const orgId = generateUUIDv7();
const resolvedOrgName = org_name || `${user.name}'s Organization`;
await c.env.ELYDORA_DB.batch([
c.env.ELYDORA_DB
.prepare(
'INSERT INTO organizations (org_id, name, created_at, updated_at) VALUES (?, ?, ?, ?)',
)
.bind(orgId, resolvedOrgName, now, now),
c.env.ELYDORA_DB
.prepare(
'UPDATE users SET org_id = ?, role = ?, status = ? WHERE user_id = ?',
)
.bind(orgId, 'org_owner', 'active', user.id),
]);
// Return the session token
const token = signUpResult.token ?? '';
return c.json({
user: {
user_id: user.id,
org_id: orgId,
email: user.email,
display_name: user.name,
role: 'org_owner',
status: 'active',
created_at: now,
updated_at: now,
},
organization: {
org_id: orgId,
name: resolvedOrgName,
created_at: now,
updated_at: now,
},
token,
}, 201);
});
// ---------------------------------------------------------------------------
// POST /v1/auth/login — Authenticate and receive a JWT
// POST /v1/auth/login — Authenticate and receive a session token
// ---------------------------------------------------------------------------
auth.post('/login', async (c) => {
const body = await c.req.json();
const { email, password } = body;
const result = await loginUser(c.env.ELYDORA_DB, c.env.JWT_SECRET, email, password);
const betterAuthInstance = createAuth(
process.env.DATABASE_URL!,
c.env.BETTER_AUTH_SECRET,
c.env.BETTER_AUTH_URL,
c.env.ALLOWED_ORIGINS,
);
return c.json(result, 200);
const signInResult = await betterAuthInstance.api.signInEmail({
body: { email, password },
});
if (!signInResult) {
const lang = c.get('lang') ?? 'en';
return c.json({ error: { code: 'UNAUTHORIZED', message: getMessage('auth.invalidCredentials', lang), request_id: '' } }, 401);
}
const user = signInResult.user as { id: string; email: string; name: string; org_id?: string; role?: string; status?: string; created_at?: number; updated_at?: number };
const token = signInResult.token ?? '';
return c.json({
user: {
user_id: user.id,
org_id: user.org_id ?? '',
email: user.email,
display_name: user.name,
role: user.role ?? 'org_owner',
status: user.status ?? 'active',
created_at: user.created_at ?? 0,
updated_at: user.updated_at ?? 0,
},
token,
}, 200);
});
// ---------------------------------------------------------------------------
@@ -57,21 +142,29 @@ auth.get('/me', authMiddleware, async (c) => {
});
// ---------------------------------------------------------------------------
// POST /v1/auth/refresh — Issue a new JWT (auth required)
// POST /v1/auth/refresh — Issue a new session token (auth required)
// ---------------------------------------------------------------------------
auth.post('/refresh', authMiddleware, async (c) => {
const now = Date.now();
const token = await issueJWT(
{
sub: c.get('actor'),
org_id: c.get('org_id'),
role: c.get('role') as RbacRole,
iat: Math.floor(now / 1000),
exp: Math.floor(now / 1000) + DEFAULT_JWT_TTL_SECONDS,
},
c.env.JWT_SECRET,
const betterAuthInstance = createAuth(
process.env.DATABASE_URL!,
c.env.BETTER_AUTH_SECRET,
c.env.BETTER_AUTH_URL,
c.env.ALLOWED_ORIGINS,
);
// Get the current session and return its token — Better Auth handles
// session refresh automatically based on the updateAge setting
const session = await betterAuthInstance.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
const lang = c.get('lang') ?? 'en';
return c.json({ error: { code: 'UNAUTHORIZED', message: getMessage('auth.invalidSession', lang), request_id: '' } }, 401);
}
const token = session.session?.token ?? '';
return c.json({ token }, 200);
});
@@ -89,15 +182,33 @@ auth.post('/token', authMiddleware, async (c) => {
}
}
const result = await issueApiToken(
c.get('actor'),
c.get('org_id'),
c.get('role') as RbacRole,
c.env.JWT_SECRET,
ttl_seconds ?? null,
// Get the current session token — Better Auth sessions have a fixed
// lifetime configured in the auth instance (7 days by default).
// The ttl_seconds parameter is accepted for API compatibility but
// session lifetime is governed by Better Auth's session config.
const betterAuthInstance = createAuth(
process.env.DATABASE_URL!,
c.env.BETTER_AUTH_SECRET,
c.env.BETTER_AUTH_URL,
c.env.ALLOWED_ORIGINS,
);
return c.json(result, 200);
const session = await betterAuthInstance.api.getSession({
headers: c.req.raw.headers,
});
if (!session) {
const lang = c.get('lang') ?? 'en';
return c.json({ error: { code: 'UNAUTHORIZED', message: getMessage('auth.invalidSession', lang), request_id: '' } }, 401);
}
const sessionData = session.session as { token?: string; expiresAt?: Date };
const token = sessionData.token ?? '';
const expiresAt = sessionData.expiresAt
? Math.floor(new Date(sessionData.expiresAt).getTime() / 1000)
: Math.floor(Date.now() / 1000) + 604800;
return c.json({ token, expires_at: expiresAt }, 200);
});
export { auth };