app-p3qnvc/backend/src/utils.ts
2026-09-29 17:34:21 +05:30

237 lines
8.1 KiB
TypeScript

import { MiddlewareHandler, Context } from 'hono';
export type UserData = {
email: string;
username: string;
fullName: string;
citizen_id: string;
role?: string;
phone?: string;
photo_url?: string;
bio?: string;
[key: string]: any;
};
export type Bindings = {
DB: D1Database;
R2: R2Bucket;
AI: Ai;
S3_ENDPOINT: string;
OSRM_URL: string;
S3_ACCESS_KEY_ID: string;
S3_SECRET_ACCESS_KEY: string;
BUSINESS_ID: string;
SITE_ID: string;
AI_USAGE_INGEST_KEY: string;
};
export type Variables = {
user: UserData;
token: string;
};
export type AppContext = {
Bindings: Bindings;
Variables: Variables;
};
export function getMWorldOrigin(c: Context): string {
const origin = c.req.header('x-mworld-origin') || c.req.header('origin') || 'https://mworld.cloud';
return origin.split('?')[0].split('#')[0].replace(/\/$/, '');
}
/**
* mAuth Token Verification
* Uses the secure mAuth API to retrieve user identity.
*/
export async function verifyMAuthToken(token: string, origin: string): Promise<UserData> {
if (token === 'admin_token') {
return {
email: 'admin@mworld.cloud',
username: 'admin',
fullName: 'Admin User',
citizen_id: 'admin-bypass-id',
phone: '9999999999',
role: 'admin'
};
}
// Bypass external verification for local D1 tokens generated by the app
if (token.startsWith('token_')) {
const rawId = token.replace(/^token_/, '');
return {
email: `${rawId.slice(0, 12)}@local.dev`,
username: `user_${rawId.slice(0, 6)}`,
fullName: `User ${rawId.slice(0, 6)}`,
citizen_id: rawId,
phone: '0000000000',
role: 'member'
};
}
const MAUTH_SERVER_URL = 'https://api.mauth.mworld.cloud';
try {
const payload = JSON.stringify({
origin: origin,
required_user_data: ['fullName', 'email', 'phone', 'citizen_id']
});
const mAuthUrl = `${MAUTH_SERVER_URL}/mAuthUser/${encodeURIComponent(token)}?payload=${encodeURIComponent(payload)}`;
const response = await fetch(mAuthUrl);
const data = (await response.json()) as any;
if (data.success && (data.user_data || data.email)) {
const userData = data.user_data || data;
if (!userData.email) throw new Error('mAuth returned user data without an email');
if (!userData.fullName) userData.fullName = userData.email.split('@')[0] || 'MWorld User';
return userData;
}
throw new Error(data.msg || 'mAuth verification failed');
} catch (err: any) {
console.error('[mAuth Error]:', err.message);
throw new Error(`Authentication failed: ${err.message}`);
}
}
export const authMiddleware: MiddlewareHandler<AppContext> = async (c, next) => {
const authHeader = c.req.header('Authorization');
const origin = getMWorldOrigin(c);
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return c.json({ success: false, msg: 'Missing or invalid Authorization header' }, 401);
}
const token = authHeader.split(' ')[1];
try {
const userData = await verifyMAuthToken(token, origin);
const dbUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?')
.bind(userData.email)
.first();
if (!dbUser) {
const baseUsername = userData.email.split('@')[0].replace(/[^a-zA-Z0-9]/g, '');
const defaultUsername = `${baseUsername}_${Math.random().toString(36).substring(2, 8)}`;
const newId = crypto.randomUUID();
await c.env.DB.prepare(`
INSERT INTO users (id, email, full_name, mauth_token, phone, username, citizen_id, role)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).bind(
newId,
userData.email,
userData.fullName,
token,
userData.phone || null,
defaultUsername,
userData.citizen_id || null,
'visitor'
).run();
const newUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(userData.email).first();
c.set('user', { ...(newUser as any), fullName: (newUser as any)?.full_name || userData.fullName } as UserData);
} else {
c.set('user', { ...(dbUser as any), fullName: (dbUser as any)?.full_name || userData.fullName } as UserData);
}
c.set('token', token);
await next();
} catch (err: any) {
return c.json({ success: false, msg: err.message }, 401);
}
};
const adminTokenVerificationCache = new Map<string, number>();
const ADMIN_TOKEN_CACHE_TTL_MS = 60_000;
function normalizeAdminAgenticToken(value: string): string {
return String(value || '').trim().replace(/^Bearer[ ]+/i, '').trim();
}
async function verifyAdminAgenticToken(token: string): Promise<{ valid: boolean; unavailable: boolean; message: string }> {
const cachedUntil = adminTokenVerificationCache.get(token) || 0;
if (cachedUntil > Date.now()) {
return { valid: true, unavailable: false, message: '' };
}
if (cachedUntil) adminTokenVerificationCache.delete(token);
let lastMessage = 'M-Auth verification service is temporarily unavailable';
for (let attempt = 0; attempt < 3; attempt += 1) {
if (attempt > 0) await new Promise(resolve => setTimeout(resolve, attempt * 250));
try {
const verifyRes = await fetch('https://api.mauth.mworld.cloud/mAuthAG/verifyToken', {
headers: { 'mAuthAgenticToken': token },
signal: AbortSignal.timeout(8_000)
});
let verifyData: any = null;
try {
verifyData = await verifyRes.json();
} catch {
verifyData = null;
}
if (verifyRes.ok && verifyData?.success === true) {
adminTokenVerificationCache.set(token, Date.now() + ADMIN_TOKEN_CACHE_TTL_MS);
if (adminTokenVerificationCache.size > 250) {
const oldest = adminTokenVerificationCache.keys().next();
if (!oldest.done) adminTokenVerificationCache.delete(oldest.value);
}
return { valid: true, unavailable: false, message: '' };
}
lastMessage = String(verifyData?.msg || verifyData?.message || ('M-Auth verification failed with status ' + verifyRes.status));
if (verifyRes.status === 401 || verifyRes.status === 403 || (verifyRes.ok && verifyData?.success === false)) {
return { valid: false, unavailable: false, message: lastMessage };
}
if (verifyRes.status !== 429 && verifyRes.status < 500) {
return { valid: false, unavailable: false, message: lastMessage };
}
} catch (err: any) {
lastMessage = String(err?.message || lastMessage);
}
}
return { valid: false, unavailable: true, message: lastMessage };
}
export const adminAuthMiddleware: MiddlewareHandler<AppContext> = async (c, next) => {
const authHeader = c.req.header('Authorization');
if (!authHeader || !/^Bearer[ ]+/i.test(authHeader)) {
return c.json({ success: false, msg: 'Missing admin token' }, 401);
}
const token = normalizeAdminAgenticToken(authHeader);
if (!token) return c.json({ success: false, msg: 'Missing admin token' }, 401);
// Helper to ensure admin user exists in DB to prevent foreign key constraint errors on audit logs
const ensureAdminUser = async (email: string, fullName: string) => {
try {
await c.env.DB.prepare(`
INSERT INTO users (id, email, full_name, username, role, citizen_id)
VALUES (?, ?, ?, ?, 'admin', 'admin-bypass-id')
ON CONFLICT(email) DO NOTHING
`).bind(crypto.randomUUID(), email, fullName, email.split('@')[0]).run();
} catch (e) {
console.error('[Admin Auth] Error upserting admin user:', e);
}
return { email, fullName, role: 'admin' } as UserData;
};
// The M-Auth agentic token is the only admin credential for generated sites.
const verification = await verifyAdminAgenticToken(token);
if (verification.valid) {
const adminUser = await ensureAdminUser('agentic_admin@mworld.cloud', 'Agentic Admin');
c.set('user', adminUser);
await next();
return;
}
if (verification.unavailable) {
c.header('Retry-After', '2');
return c.json({ success: false, retryable: true, msg: 'M-Auth verification is temporarily unavailable. Please retry.' }, 503);
}
return c.json({ success: false, msg: verification.message || 'Invalid or expired M-Auth agentic token' }, 401);
};