237 lines
8.1 KiB
TypeScript
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);
|
|
}; |