Split the 657-line operation service into eor, submit, and verify modules, add the AGENT_REVOKED and KEY_RETIRED codes the Backend already emits, and condense every multi-line comment. Mirror Node, Python, and Go at 2.1.0 with the deep-health 503, non-idempotent retry, rotation grace, and path-encoding fixes. Document which routes and response fields the self-hosted server does not provide. Claude-Session: https://claude.ai/code/session_01Agoc9bRVE9KL7mmWtey9Vq
195 lines
6.6 KiB
TypeScript
195 lines
6.6 KiB
TypeScript
/** Auth routes: registration, login, profile retrieval, and token refresh. */
|
|
|
|
import { Hono } from 'hono';
|
|
import type { Env, AppVariables } from '../types.js';
|
|
import { authMiddleware } from '../middleware/auth.js';
|
|
import { AppError } from '../middleware/error-handler.js';
|
|
import { issueApiToken, MAX_TTL_SECONDS } from '../services/api-token-service.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 }>();
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST /v1/auth/register: Create a new user and organization
|
|
// ---------------------------------------------------------------------------
|
|
auth.post('/register', async (c) => {
|
|
const body = await c.req.json();
|
|
const { email, password, display_name, org_name } = body;
|
|
|
|
const betterAuthInstance = createAuth(
|
|
process.env.DATABASE_URL!,
|
|
c.env.BETTER_AUTH_SECRET,
|
|
c.env.BETTER_AUTH_URL,
|
|
c.env.ALLOWED_ORIGINS,
|
|
);
|
|
|
|
// 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 session token
|
|
// ---------------------------------------------------------------------------
|
|
auth.post('/login', async (c) => {
|
|
const body = await c.req.json();
|
|
const { email, password } = body;
|
|
|
|
const betterAuthInstance = createAuth(
|
|
process.env.DATABASE_URL!,
|
|
c.env.BETTER_AUTH_SECRET,
|
|
c.env.BETTER_AUTH_URL,
|
|
c.env.ALLOWED_ORIGINS,
|
|
);
|
|
|
|
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);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// GET /v1/auth/me: Get current user profile (auth required)
|
|
// ---------------------------------------------------------------------------
|
|
auth.get('/me', authMiddleware, async (c) => {
|
|
const userId = c.get('actor');
|
|
const user = await getMe(c.env.ELYDORA_DB, userId);
|
|
|
|
return c.json({ user }, 200);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST /v1/auth/refresh: Issue a new session token (auth required)
|
|
// ---------------------------------------------------------------------------
|
|
auth.post('/refresh', authMiddleware, async (c) => {
|
|
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);
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// POST /v1/auth/token: Issue an opaque API token (session required)
|
|
// ---------------------------------------------------------------------------
|
|
auth.post('/token', authMiddleware, async (c) => {
|
|
if (c.get('auth_token_type') !== 'session') {
|
|
throw new AppError(400, 'VALIDATION_ERROR', { key: 'auth.issueRequiresSession' });
|
|
}
|
|
|
|
const body = await c.req.json();
|
|
const { ttl_seconds } = body;
|
|
|
|
if (ttl_seconds !== null && ttl_seconds !== undefined) {
|
|
if (typeof ttl_seconds !== 'number') {
|
|
throw new AppError(400, 'VALIDATION_ERROR', { key: 'auth.ttlMustBeNumber' });
|
|
}
|
|
if (!Number.isInteger(ttl_seconds) || ttl_seconds <= 0 || ttl_seconds > MAX_TTL_SECONDS) {
|
|
throw new AppError(400, 'VALIDATION_ERROR', { key: 'auth.ttlMustBePositive' });
|
|
}
|
|
}
|
|
|
|
const orgId = c.get('org_id');
|
|
if (!orgId) {
|
|
throw new AppError(409, 'VALIDATION_ERROR', { key: 'auth.organizationRequiredForApiToken' });
|
|
}
|
|
|
|
const issued = await issueApiToken(c.env.ELYDORA_DB, c.get('actor'), orgId, ttl_seconds ?? null);
|
|
return c.json({ token: issued.token, expires_at: issued.expires_at }, 200);
|
|
});
|
|
|
|
export { auth };
|