feat: add backend logic in /backend

This commit is contained in:
MWorld Deployer 2026-09-29 20:05:51 +05:30
parent 36f6bc3c1f
commit 59fbe66793
8 changed files with 2988 additions and 0 deletions

1499
backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

21
backend/src/index.ts Normal file
View File

@ -0,0 +1,21 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { AppContext } from './utils';
import publicRoutes from './routes/public';
import adminRoutes from './routes/admin';
import { agentic } from './routes/external';
const app = new Hono<AppContext>();
app.use('*', cors({
origin: '*',
allowHeaders: ['Content-Type', 'Authorization', 'X-MAuth-Token', 'x-mworld-origin', 'mAuthAgenticToken'],
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
}));
app.route('/', agentic);
app.route('/api', publicRoutes);
app.route('/api/admin', adminRoutes);
export default app;

View File

@ -0,0 +1,68 @@
import { Hono } from 'hono';
import { AppContext, adminAuthMiddleware } from '../utils';
const adminRoutes = new Hono<AppContext>();
adminRoutes.use('*', adminAuthMiddleware);
adminRoutes.get('/verify-token', (c) => {
return c.json({ success: true, valid: true });
});
adminRoutes.get('/users', async (c) => {
const { results } = await c.env.DB.prepare('SELECT * FROM users ORDER BY created_at DESC').all();
return c.json({ success: true, users: results || [] });
});
adminRoutes.get('/stats', async (c) => {
const totalUsers = await c.env.DB.prepare('SELECT COUNT(*) as cnt FROM users').first<any>();
const activeDrivers = await c.env.DB.prepare('SELECT COUNT(*) as cnt FROM driver_profiles WHERE is_available = 1').first<any>();
const totalRides = await c.env.DB.prepare('SELECT COUNT(*) as cnt FROM rides').first<any>();
const activeRides = await c.env.DB.prepare("SELECT COUNT(*) as cnt FROM rides WHERE status IN ('requested', 'accepted', 'picked_up')").first<any>();
const completedRides = await c.env.DB.prepare("SELECT COUNT(*) as cnt FROM rides WHERE status = 'completed'").first<any>();
return c.json({
success: true,
stats: {
total_users: totalUsers?.cnt || 0,
active_drivers: activeDrivers?.cnt || 0,
total_rides: totalRides?.cnt || 0,
active_rides: activeRides?.cnt || 0,
completed_rides: completedRides?.cnt || 0
}
});
});
adminRoutes.get('/rides', async (c) => {
const { results } = await c.env.DB.prepare(`
SELECT r.*,
p.full_name as passenger_name, p.phone as passenger_phone,
d.full_name as driver_name, d.phone as driver_phone
FROM rides r
LEFT JOIN users p ON r.passenger_email = p.email
LEFT JOIN users d ON r.driver_email = d.email
ORDER BY r.created_at DESC
`).all();
return c.json({ success: true, rides: results || [] });
});
adminRoutes.post('/upload', async (c) => {
try {
const body = await c.req.parseBody();
const file = body['file'];
if (!file || !(file instanceof File)) {
return c.json({ success: false, msg: 'No file provided' }, 400);
}
const ext = file.name.split('.').pop() || 'bin';
const key = crypto.randomUUID() + '.' + ext;
await c.env.R2.put(key, await file.arrayBuffer(), {
httpMetadata: { contentType: file.type }
});
return c.json({ success: true, url: '/api/media/' + key });
} catch (error: any) {
return c.json({ success: false, msg: error.message }, 500);
}
});
export default adminRoutes;

View File

@ -0,0 +1,205 @@
import { Hono } from 'hono';
import { cors } from 'hono/cors';
import { AppContext } from '../utils';
// ============================================================
// mAgenticAuth — MirrorWorld system-level auth for skills.
// Token passed as ?mAgenticAuthToken=... query param.
// The valid token is injected via env: c.env.M_AGENTIC_AUTH_TOKEN
// End users never see or use these routes.
// ============================================================
function verifyAgenticToken(c: any): boolean {
const provided = c.req.query('mAgenticAuthToken') || c.req.query('mAuthAgenticToken');
const expected = c.env.M_AGENTIC_AUTH_TOKEN;
if (!expected || !provided) return false;
return decodeURIComponent(provided) === decodeURIComponent(expected);
}
function agenticGuard(c: any): Response | null {
if (!verifyAgenticToken(c)) {
return c.json({ success: false, error: 'Invalid or missing mAgenticAuthToken' }, 401);
}
return null;
}
// MirrorWorld standard response format
function mwResponse(action: string, logs: string[], data: any) {
const now = () => new Date().toISOString();
return {
success: true,
response: [
{ level: 'info', message: `Direct action request to /${action}`, timestamp: now() },
...logs.map(msg => ({ level: 'info', message: msg, timestamp: now() })),
],
data,
};
}
// Discover all real business-data tables (exclude system/internal tables)
async function discoverTables(db: any): Promise<string[]> {
const BLOCKED = new Set([
'users', 'api_keys', 'webhooks', 'webhook_deliveries',
'sqlite_master', 'sqlite_sequence', 'notifications',
'follows', 'profile_activity', 'changelogs', 'settings', 'signaling',
]);
const res = await db.prepare(
`SELECT name FROM sqlite_master
WHERE type='table'
AND name NOT LIKE 'sqlite_%'
AND name NOT LIKE '%_fts%'
ORDER BY name`
).all();
return res.results.map((t: any) => t.name).filter((n: string) => !BLOCKED.has(n));
}
async function isValidEntity(db: any, entity: string): Promise<boolean> {
if (!/^[a-z_][a-z0-9_]*$/i.test(entity)) return false;
const tables = await discoverTables(db);
return tables.includes(entity);
}
export const agentic = new Hono<AppContext>();
agentic.use('*', cors({ origin: '*', allowMethods: ['GET', 'POST', 'OPTIONS'], allowHeaders: ['*'] }));
// ============================================================
// RESERVED ENDPOINT: /mcp
// Returns the manifest of callable actions. Auto-generated
// from the app's real tables — fully generic, no hardcoding.
// ============================================================
agentic.get('/mcp', async (c) => {
const denied = agenticGuard(c); if (denied) return denied;
const tables = await discoverTables(c.env.DB);
const siteName = 'Generated App';
const siteDesc = 'Execute operations on this application through a single manifest-driven endpoint.';
const paths: Record<string, any> = {};
// Generate list/create/update/delete actions for EVERY real entity
for (const t of tables) {
paths[`/list_${t}`] = {
description: `List all records in the "${t}" entity.`,
post: { requestBody: {
prompt: { type: 'string', description: 'Natural-language instruction for the operation.', required: true },
limit: { type: 'number', description: 'Max records to return (default 50).', required: false },
page: { type: 'number', description: 'Page number.', required: false },
} },
};
paths[`/create_${t}`] = {
description: `Create a new record in the "${t}" entity. Pass the record fields in the body.`,
post: { requestBody: {
prompt: { type: 'string', description: 'Natural-language instruction for the operation.', required: true },
data: { type: 'object', description: `The ${t} record fields to insert.`, required: false },
} },
};
paths[`/update_${t}`] = {
description: `Update an existing record in the "${t}" entity by id.`,
post: { requestBody: {
prompt: { type: 'string', description: 'Natural-language instruction for the operation.', required: true },
id: { type: 'string', description: 'The record id to update.', required: true },
data: { type: 'object', description: 'Fields to update.', required: false },
} },
};
paths[`/delete_${t}`] = {
description: `Delete a record from the "${t}" entity by id.`,
post: { requestBody: {
prompt: { type: 'string', description: 'Natural-language instruction for the operation.', required: true },
id: { type: 'string', description: 'The record id to delete.', required: true },
} },
};
}
return c.json({ title: `${siteName} MCP`, description: siteDesc, paths });
});
// ============================================================
// GENERIC ACTION HANDLER
// Handles /list_X, /create_X, /update_X, /delete_X for any entity,
// plus /get_app_details and /get_daily_report.
// Injected data goes into the REAL tables → appears as normal app data.
// ============================================================
const handleAction = async (c: any, action: string) => {
const denied = agenticGuard(c); if (denied) return denied;
const clean = action.replace(/^\//, '');
const body = await c.req.json().catch(() => ({}));
// ── LIST ──
const listM = clean.match(/^list_(.+)$/);
if (listM) {
const entity = listM[1];
if (!(await isValidEntity(c.env.DB, entity))) {
return c.json({ success: false, error: `Unknown entity: ${entity}` }, 404);
}
const limit = Math.min(100, Number(body.limit) || 50);
const page = Math.max(1, Number(body.page) || 1);
const rows = await c.env.DB.prepare(
`SELECT * FROM ${entity} ORDER BY created_at DESC LIMIT ? OFFSET ?`
).bind(limit, (page - 1) * limit).all().catch(() => ({ results: [] }));
return c.json(mwResponse(clean, [`Fetched ${rows.results.length} records from "${entity}"`], {
[entity]: rows.results,
count: rows.results.length,
}));
}
// ── CREATE (inject data — appears as normal app data) ──
const createM = clean.match(/^create_(.+)$/);
if (createM) {
const entity = createM[1];
if (!(await isValidEntity(c.env.DB, entity))) {
return c.json({ success: false, error: `Unknown entity: ${entity}` }, 404);
}
const record = { ...(body.data || body) };
delete record.prompt;
if (!record.id) record.id = crypto.randomUUID();
if (!record.created_at) record.created_at = new Date().toISOString().replace('T', ' ').slice(0, 19);
const cols = Object.keys(record).filter(k => /^[a-z_][a-z0-9_]*$/i.test(k));
if (cols.length) {
const placeholders = cols.map(() => '?').join(', ');
await c.env.DB.prepare(
`INSERT INTO ${entity} (${cols.join(', ')}) VALUES (${placeholders})`
).bind(...cols.map(k => record[k])).run().catch((e: any) => { throw e; });
}
return c.json(mwResponse(clean, [`Injected new record into "${entity}"`], record));
}
// ── UPDATE ──
const updateM = clean.match(/^update_(.+)$/);
if (updateM) {
const entity = updateM[1];
if (!(await isValidEntity(c.env.DB, entity))) {
return c.json({ success: false, error: `Unknown entity: ${entity}` }, 404);
}
const id = body.id;
if (!id) return c.json({ success: false, error: 'id is required' }, 400);
const data = { ...(body.data || {}) };
delete data.id; delete data.prompt;
const cols = Object.keys(data).filter(k => /^[a-z_][a-z0-9_]*$/i.test(k));
if (!cols.length) return c.json({ success: false, error: 'No fields to update' }, 400);
await c.env.DB.prepare(
`UPDATE ${entity} SET ${cols.map(k => `${k} = ?`).join(', ')} WHERE id = ?`
).bind(...cols.map(k => data[k]), id).run().catch(() => {});
const updated = await c.env.DB.prepare(`SELECT * FROM ${entity} WHERE id = ?`).bind(id).first();
return c.json(mwResponse(clean, [`Updated record ${id} in "${entity}"`], updated));
}
// ── DELETE ──
const deleteM = clean.match(/^delete_(.+)$/);
if (deleteM) {
const entity = deleteM[1];
if (!(await isValidEntity(c.env.DB, entity))) {
return c.json({ success: false, error: `Unknown entity: ${entity}` }, 404);
}
const id = body.id;
if (!id) return c.json({ success: false, error: 'id is required' }, 400);
await c.env.DB.prepare(`DELETE FROM ${entity} WHERE id = ?`).bind(id).run().catch(() => {});
return c.json(mwResponse(clean, [`Deleted record ${id} from "${entity}"`], { id, status: 'deleted' }));
}
return c.json(mwResponse(clean, [`Unknown action "${clean}"`], { status: 'noop' }));
};
// All actions accept both GET and POST, token via query param
agentic.post('/:action', (c) => handleAction(c, c.req.param('action')));
agentic.get('/:action', (c) => handleAction(c, c.req.param('action')));

View File

@ -0,0 +1,694 @@
import { Hono } from 'hono';
import { AppContext, authMiddleware, getMWorldOrigin, verifyMAuthToken, UserData } from '../utils';
const publicRoutes = new Hono<AppContext>();
publicRoutes.get('/media/*', async (c) => {
const encodedKey = c.req.path.split('/media/')[1] || '';
const key = decodeURIComponent(encodedKey);
if (!key) return c.json({ success: false, msg: 'Not found' }, 404);
const object = await c.env.R2.get(key);
if (!object) return c.json({ success: false, msg: 'Not found' }, 404);
const headers = new Headers();
object.writeHttpMetadata(headers);
headers.set('etag', object.httpEtag);
headers.set('Accept-Ranges', 'bytes');
headers.set('Cache-Control', 'public, max-age=31536000');
const rangeHeader = c.req.header('Range');
if (rangeHeader) {
const parts = rangeHeader.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1] ? parseInt(parts[1], 10) : object.size - 1;
if (!Number.isFinite(start) || !Number.isFinite(end) || start < 0 || end < start || start >= object.size || end >= object.size) {
return new Response('Requested Range Not Satisfiable', { status: 416 });
}
headers.set('Content-Range', `bytes ${start}-${end}/${object.size}`);
headers.set('Content-Length', (end - start + 1).toString());
const rangeObject = await c.env.R2.get(key, { range: { offset: start, length: end - start + 1 } });
if (!rangeObject) return c.json({ success: false, msg: 'Not found' }, 404);
return new Response(rangeObject.body, { status: 206, headers });
}
headers.set('Content-Length', object.size.toString());
return new Response(object.body, { headers });
});
publicRoutes.get('/health', (c) => {
return c.json({ status: 'ok', timestamp: new Date().toISOString() });
});
publicRoutes.get('/notifications/count', authMiddleware, async (c) => {
const user = c.get('user');
const count = await c.env.DB.prepare('SELECT COUNT(*) as c FROM notifications WHERE recipient_email = ? AND is_read = 0')
.bind(user.email || null)
.first<any>()
.catch(() => ({ c: 0 }));
return c.json({ count: count?.c || 0 });
});
publicRoutes.post('/user/check-in', async (c) => {
const body = await c.req.json().catch(() => ({}));
const token = body.mauth_token || body.user_access_token;
if (!token) {
return c.json({ success: false, msg: "Unauthorized: Missing mauth_token" }, 401);
}
const origin = getMWorldOrigin(c);
let userData: UserData;
try {
userData = await verifyMAuthToken(token, origin);
} catch (e: any) {
return c.json({ success: false, msg: e.message || "Unauthorized" }, 401);
}
if (!userData || !userData.email) {
return c.json({ success: false, msg: "Invalid token or missing email" }, 401);
}
const now = new Date().toISOString();
const existing = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?')
.bind(userData.email)
.first<any>();
const role = existing?.role || userData.role || 'passenger';
const username = existing?.username || (userData.email.split('@')[0] + '_' + Math.floor(Math.random() * 10000));
const id = existing?.id || crypto.randomUUID();
if (existing) {
await c.env.DB.prepare(
`UPDATE users SET full_name = ?, phone = ?, citizen_id = ?, mauth_token = ?, updated_at = ? WHERE email = ?`
).bind(userData.fullName || userData.full_name || existing.full_name || null, userData.phone || existing.phone || null, userData.citizen_id || existing.citizen_id || null, token, now, userData.email).run();
} else {
await c.env.DB.prepare(
`INSERT INTO users (id, email, full_name, role, created_at, updated_at, username, mauth_token, phone, citizen_id, is_banned)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 0)`
).bind(id, userData.email, userData.fullName || userData.full_name || null, role, now, now, username, token, userData.phone || null, userData.citizen_id || null).run();
}
const user = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?').bind(userData.email).first();
return c.json({ success: true, user });
});
publicRoutes.get('/companion/safety-profile', authMiddleware, async (c) => {
const user = c.get('user');
const profile = await c.env.DB.prepare('SELECT * FROM companion_safety_profiles WHERE user_email = ?')
.bind(user.email || null)
.first();
return c.json({ success: true, profile: profile || null });
});
publicRoutes.put('/companion/safety-profile', authMiddleware, async (c) => {
const user = c.get('user');
const body = await c.req.json().catch(() => ({}));
const persona_label = body.persona_label;
const safety_consent = body.safety_consent ? 1 : 0;
if (!persona_label) {
return c.json({ success: false, msg: "persona_label is required" }, 400);
}
const now = new Date().toISOString();
const existing = await c.env.DB.prepare('SELECT * FROM companion_safety_profiles WHERE user_email = ?')
.bind(user.email || null)
.first<any>();
if (existing) {
await c.env.DB.prepare(
`UPDATE companion_safety_profiles SET persona_label = ?, safety_consent = ?, updated_at = ? WHERE user_email = ?`
).bind(persona_label, safety_consent, now, user.email || null).run();
} else {
await c.env.DB.prepare(
`INSERT INTO companion_safety_profiles (user_email, persona_label, safety_consent, background_check_status, created_at, updated_at)
VALUES (?, ?, ?, 'not_started', ?, ?)`
).bind(user.email || null, persona_label, safety_consent, now, now).run();
}
const profile = await c.env.DB.prepare('SELECT * FROM companion_safety_profiles WHERE user_email = ?')
.bind(user.email || null)
.first();
return c.json({ success: true, profile });
});
publicRoutes.post('/companion/safety-profile/background-check', authMiddleware, async (c) => {
const user = c.get('user');
const body = await c.req.json().catch(() => ({}));
if (body.consent !== true && body.consent !== 1) {
return c.json({ success: false, msg: "Explicit consent is required to request background check" }, 400);
}
const now = new Date().toISOString();
const existing = await c.env.DB.prepare('SELECT * FROM companion_safety_profiles WHERE user_email = ?')
.bind(user.email || null)
.first<any>();
if (existing) {
await c.env.DB.prepare(
`UPDATE companion_safety_profiles SET background_check_status = 'pending', updated_at = ? WHERE user_email = ?`
).bind(now, user.email || null).run();
} else {
await c.env.DB.prepare(
`INSERT INTO companion_safety_profiles (user_email, persona_label, safety_consent, background_check_status, created_at, updated_at)
VALUES (?, 'Default Persona', 1, 'pending', ?, ?)`
).bind(user.email || null, now, now).run();
}
return c.json({ success: true, status: 'pending' });
});
publicRoutes.get('/companion/circles/my', authMiddleware, async (c) => {
const user = c.get('user');
const circles = await c.env.DB.prepare(
`SELECT c.* FROM companion_circles c
JOIN companion_circle_members m ON c.id = m.circle_id
WHERE m.user_email = ?`
).bind(user.email || null).all<any>();
const results = [];
for (const circle of (circles.results || [])) {
const members = await c.env.DB.prepare(
`SELECT m.*, u.full_name, u.username FROM companion_circle_members m
LEFT JOIN users u ON m.user_email = u.email
WHERE m.circle_id = ?`
).bind(circle.id).all();
results.push({
...circle,
members: members.results || []
});
}
return c.json({ success: true, circles: results });
});
publicRoutes.post('/companion/circles/:id/respond', authMiddleware, async (c) => {
const user = c.get('user');
const { id } = c.req.param();
const body = await c.req.json().catch(() => ({}));
const decision = (body.decision || '').toLowerCase();
if (!['accepted', 'declined'].includes(decision)) {
return c.json({ success: false, msg: "decision must be 'accepted' or 'declined'" }, 400);
}
const now = new Date().toISOString();
const member = await c.env.DB.prepare(
`SELECT * FROM companion_circle_members WHERE circle_id = ? AND user_email = ?`
).bind(id, user.email || null).first();
if (!member) {
return c.json({ success: false, msg: "Circle invitation not found" }, 404);
}
await c.env.DB.prepare(
`UPDATE companion_circle_members SET status = ?, updated_at = ? WHERE circle_id = ? AND user_email = ?`
).bind(decision, now, id, user.email || null).run();
const circle = await c.env.DB.prepare('SELECT * FROM companion_circles WHERE id = ?').bind(id).first<any>();
if (!circle) {
return c.json({ success: false, msg: "Circle not found" }, 404);
}
const members = await c.env.DB.prepare(
`SELECT m.*, u.full_name, u.username FROM companion_circle_members m
LEFT JOIN users u ON m.user_email = u.email
WHERE m.circle_id = ?`
).bind(id).all();
return c.json({
success: true,
circle: {
...circle,
members: members.results || []
}
});
});
publicRoutes.get('/companion/circles/:id/activities', authMiddleware, async (c) => {
const user = c.get('user');
const { id } = c.req.param();
const member = await c.env.DB.prepare(
`SELECT * FROM companion_circle_members WHERE circle_id = ? AND user_email = ? AND status = 'accepted'`
).bind(id, user.email || null).first();
if (!member) {
return c.json({ success: false, msg: "Only accepted circle members can view activities" }, 403);
}
const activities = await c.env.DB.prepare(
`SELECT * FROM companion_activities WHERE circle_id = ? ORDER BY starts_at ASC`
).bind(id).all();
return c.json({ success: true, activities: activities.results || [] });
});
publicRoutes.post('/companion/circles/:id/activities', authMiddleware, async (c) => {
const user = c.get('user');
const { id } = c.req.param();
const member = await c.env.DB.prepare(
`SELECT * FROM companion_circle_members WHERE circle_id = ? AND user_email = ? AND status = 'accepted'`
).bind(id, user.email || null).first();
if (!member) {
return c.json({ success: false, msg: "Only accepted circle members can create activities" }, 403);
}
const body = await c.req.json().catch(() => ({}));
if (!body.title || !body.location_label || !body.starts_at) {
return c.json({ success: false, msg: "title, location_label, and starts_at are required" }, 400);
}
const activityId = crypto.randomUUID();
const now = new Date().toISOString();
await c.env.DB.prepare(
`INSERT INTO companion_activities (id, circle_id, created_by_email, title, description, location_label, starts_at, max_attendees, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'planned', ?, ?)`
).bind(activityId, id, user.email || null, body.title || null, body.description || null, body.location_label || null, body.starts_at || null, body.max_attendees ? Number(body.max_attendees || null) : null,
now,
now
).run();
const activity = await c.env.DB.prepare('SELECT * FROM companion_activities WHERE id = ?').bind(activityId).first();
return c.json({ success: true, activity });
});
publicRoutes.post('/companion/activities/:id/rsvp', authMiddleware, async (c) => {
const user = c.get('user');
const { id } = c.req.param();
const body = await c.req.json().catch(() => ({}));
const status = (body.status || 'going').toLowerCase();
if (!['going', 'maybe', 'not_going'].includes(status)) {
return c.json({ success: false, msg: "status must be 'going', 'maybe', or 'not_going'" }, 400);
}
const activity = await c.env.DB.prepare('SELECT * FROM companion_activities WHERE id = ?').bind(id).first<any>();
if (!activity) {
return c.json({ success: false, msg: "Activity not found" }, 404);
}
const member = await c.env.DB.prepare(
`SELECT * FROM companion_circle_members WHERE circle_id = ? AND user_email = ? AND status = 'accepted'`
).bind(activity.circle_id, user.email || null).first();
if (!member) {
return c.json({ success: false, msg: "Must be an accepted circle member to RSVP" }, 403);
}
if (status === 'going' && activity.max_attendees) {
const currentCount = await c.env.DB.prepare(
`SELECT COUNT(*) as cnt FROM companion_activity_rsvps WHERE activity_id = ? AND status = 'going' AND user_email != ?`
).bind(id, user.email || null).first<any>();
if ((currentCount?.cnt || 0) >= activity.max_attendees) {
return c.json({ success: false, msg: "Activity maximum capacity reached" }, 400);
}
}
const now = new Date().toISOString();
const existing = await c.env.DB.prepare(
`SELECT * FROM companion_activity_rsvps WHERE activity_id = ? AND user_email = ?`
).bind(id, user.email || null).first();
if (existing) {
await c.env.DB.prepare(
`UPDATE companion_activity_rsvps SET status = ?, updated_at = ? WHERE activity_id = ? AND user_email = ?`
).bind(status, now, id, user.email || null).run();
} else {
await c.env.DB.prepare(
`INSERT INTO companion_activity_rsvps (activity_id, user_email, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?)`
).bind(id, user.email || null, status, now, now).run();
}
const rsvp = await c.env.DB.prepare(
`SELECT * FROM companion_activity_rsvps WHERE activity_id = ? AND user_email = ?`
).bind(id, user.email || null).first();
return c.json({ success: true, rsvp });
});
publicRoutes.get('/driver/profile', authMiddleware, async (c) => {
const user = c.get('user');
const profile = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first();
return c.json({ success: true, profile: profile || null });
});
publicRoutes.post('/driver/profile', authMiddleware, async (c) => {
const user = c.get('user');
const body = await c.req.json().catch(() => ({}));
if (!body.vehicle_make || !body.vehicle_model || !body.license_plate || !body.vehicle_color) {
return c.json({ success: false, msg: "vehicle_make, vehicle_model, license_plate, and vehicle_color are required" }, 400);
}
const now = new Date().toISOString();
const existing = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first<any>();
const id = existing?.id || crypto.randomUUID();
if (existing) {
await c.env.DB.prepare(
`UPDATE driver_profiles SET vehicle_make = ?, vehicle_model = ?, license_plate = ?, vehicle_color = ?, updated_at = ? WHERE user_email = ?`
).bind(body.vehicle_make || null, body.vehicle_model || null, body.license_plate || null, body.vehicle_color || null, now, user.email || null).run();
} else {
await c.env.DB.prepare(
`INSERT INTO driver_profiles (id, user_email, vehicle_make, vehicle_model, license_plate, vehicle_color, is_available, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`
).bind(id, user.email || null, body.vehicle_make || null, body.vehicle_model || null, body.license_plate || null, body.vehicle_color || null, now, now).run();
}
await c.env.DB.prepare(`UPDATE users SET role = 'driver', updated_at = ? WHERE email = ?`).bind(now, user.email || null).run();
const profile = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first();
return c.json({ success: true, profile });
});
publicRoutes.put('/driver/availability', authMiddleware, async (c) => {
const user = c.get('user');
const body = await c.req.json().catch(() => ({}));
const isAvailable = body.is_available ? 1 : 0;
const now = new Date().toISOString();
await c.env.DB.prepare('UPDATE driver_profiles SET is_available = ?, updated_at = ? WHERE user_email = ?')
.bind(isAvailable, now, user.email || null)
.run();
const profile = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first();
return c.json({ success: true, profile });
});
publicRoutes.get('/rides/nearby', authMiddleware, async (c) => {
const rides = await c.env.DB.prepare("SELECT * FROM rides WHERE status = 'requested' ORDER BY created_at DESC").all();
return c.json({ success: true, rides: rides.results || [] });
});
publicRoutes.get('/rides/my', authMiddleware, async (c) => {
const user = c.get('user');
const rides = await c.env.DB.prepare(
"SELECT * FROM rides WHERE passenger_email = ? OR driver_email = ? ORDER BY created_at DESC"
).bind(user.email || null, user.email || null).all();
return c.json({ success: true, rides: rides.results || [] });
});
publicRoutes.get('/rides/:id', authMiddleware, async (c) => {
const { id } = c.req.param();
const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first();
if (!ride) {
return c.json({ success: false, msg: "Ride not found" }, 404);
}
return c.json({ success: true, ride });
});
publicRoutes.post('/rides', authMiddleware, async (c) => {
const user = c.get('user');
const body = await c.req.json().catch(() => ({}));
if (!body.pickup_location || body.pickup_lat === undefined || body.pickup_lng === undefined ||
!body.destination_location || body.destination_lat === undefined || body.destination_lng === undefined) {
return c.json({ success: false, msg: "Pickup and destination location details are required" }, 400);
}
const id = crypto.randomUUID();
const now = new Date().toISOString();
await c.env.DB.prepare(
`INSERT INTO rides (id, passenger_email, pickup_location, pickup_lat, pickup_lng, destination_location, destination_lat, destination_lng, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'requested', ?, ?)`
).bind(id, user.email || null, body.pickup_location || null, Number(body.pickup_lat || null),
Number(body.pickup_lng),
body.destination_location,
Number(body.destination_lat),
Number(body.destination_lng),
now,
now
).run();
const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first();
return c.json({ success: true, ride });
});
publicRoutes.post('/rides/:id/accept', authMiddleware, async (c) => {
const user = c.get('user');
const { id } = c.req.param();
const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first<any>();
if (!ride) {
return c.json({ success: false, msg: "Ride not found" }, 404);
}
if (ride.status !== 'requested') {
return c.json({ success: false, msg: "Ride is no longer available" }, 400);
}
const driverProfile = await c.env.DB.prepare('SELECT * FROM driver_profiles WHERE user_email = ?').bind(user.email || null).first<any>();
const now = new Date().toISOString();
await c.env.DB.prepare(
`UPDATE rides SET driver_email = ?, status = 'accepted', driver_lat = ?, driver_lng = ?, updated_at = ? WHERE id = ?`
).bind(user.email || null, driverProfile?.current_lat ?? ride.pickup_lat, driverProfile?.current_lng ?? ride.pickup_lng, now, id).run();
const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first();
return c.json({ success: true, ride: updatedRide });
});
publicRoutes.post('/rides/:id/status', authMiddleware, async (c) => {
const user = c.get('user');
const { id } = c.req.param();
const body = await c.req.json().catch(() => ({}));
const newStatus = (body.status || '').toLowerCase();
if (!['picked_up', 'completed'].includes(newStatus)) {
return c.json({ success: false, msg: "status must be 'picked_up' or 'completed'" }, 400);
}
const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first<any>();
if (!ride) {
return c.json({ success: false, msg: "Ride not found" }, 404);
}
if (ride.driver_email !== user.email) {
return c.json({ success: false, msg: "Only the assigned driver can update ride status" }, 403);
}
if (newStatus === 'picked_up' && ride.status !== 'accepted') {
return c.json({ success: false, msg: "Ride must be accepted before picking up passenger" }, 400);
}
if (newStatus === 'completed' && ride.status !== 'picked_up') {
return c.json({ success: false, msg: "Ride must be picked up before completing" }, 400);
}
const now = new Date().toISOString();
await c.env.DB.prepare('UPDATE rides SET status = ?, updated_at = ? WHERE id = ?').bind(newStatus, now, id).run();
const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first();
return c.json({ success: true, ride: updatedRide });
});
publicRoutes.post('/rides/:id/location', authMiddleware, async (c) => {
const user = c.get('user');
const { id } = c.req.param();
const body = await c.req.json().catch(() => ({}));
if (body.driver_lat === undefined || body.driver_lng === undefined) {
return c.json({ success: false, msg: "driver_lat and driver_lng are required" }, 400);
}
const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first<any>();
if (!ride) {
return c.json({ success: false, msg: "Ride not found" }, 404);
}
const lat = Number(body.driver_lat);
const lng = Number(body.driver_lng);
const now = new Date().toISOString();
await c.env.DB.prepare('UPDATE rides SET driver_lat = ?, driver_lng = ?, updated_at = ? WHERE id = ?').bind(lat, lng, now, id).run();
await c.env.DB.prepare('UPDATE driver_profiles SET current_lat = ?, current_lng = ?, updated_at = ? WHERE user_email = ?').bind(lat, lng, now, user.email || null).run();
const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first();
return c.json({ success: true, ride: updatedRide });
});
publicRoutes.post('/rides/:id/cancel', authMiddleware, async (c) => {
const { id } = c.req.param();
const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first<any>();
if (!ride) {
return c.json({ success: false, msg: "Ride not found" }, 404);
}
if (!['requested', 'accepted'].includes(ride.status)) {
return c.json({ success: false, msg: "Cannot cancel ride once trip has started or completed" }, 400);
}
const now = new Date().toISOString();
await c.env.DB.prepare("UPDATE rides SET status = 'cancelled', updated_at = ? WHERE id = ?").bind(now, id).run();
const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first();
return c.json({ success: true, ride: updatedRide });
});
publicRoutes.post('/rides/:id/rate', authMiddleware, async (c) => {
const user = c.get('user');
const { id } = c.req.param();
const body = await c.req.json().catch(() => ({}));
const rating = Number(body.rating);
if (!rating || rating < 1 || rating > 5) {
return c.json({ success: false, msg: "Rating must be an integer between 1 and 5" }, 400);
}
const ride = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first<any>();
if (!ride) {
return c.json({ success: false, msg: "Ride not found" }, 404);
}
if (ride.passenger_email !== user.email) {
return c.json({ success: false, msg: "Only the passenger can rate this ride" }, 403);
}
const now = new Date().toISOString();
await c.env.DB.prepare('UPDATE rides SET rating = ?, updated_at = ? WHERE id = ?').bind(rating, now, id).run();
const updatedRide = await c.env.DB.prepare('SELECT * FROM rides WHERE id = ?').bind(id).first();
return c.json({ success: true, ride: updatedRide });
});
publicRoutes.post('/orders/create', authMiddleware, async (c) => {
const user = c.get('user');
const body = await c.req.json().catch(() => ({}));
const amount = Number(body.amount);
if (!amount || amount <= 0) {
return c.json({ success: false, msg: "Valid payment amount is required" }, 400);
}
const orderId = 'ord_' + crypto.randomUUID().replace(/-/g, '').slice(0, 16);
const paymentSessionId = 'sess_' + crypto.randomUUID().replace(/-/g, '').slice(0, 16);
const now = new Date().toISOString();
let cfSessionId = paymentSessionId;
try {
const cfRes = await fetch('https://sandbox.cashfree.com/pg/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-api-version': '2022-09-01',
'x-client-id': (c.env as any).CASHFREE_APP_ID || 'sandbox_app_id',
'x-client-secret': (c.env as any).CASHFREE_SECRET_KEY || 'sandbox_secret'
},
body: JSON.stringify({
order_id: orderId,
order_amount: amount,
order_currency: 'INR',
customer_details: {
customer_id: user.email.replace(/[^a-zA-Z0-9]/g, '_'),
customer_email: user.email,
customer_phone: user.phone || '9999999999'
},
order_meta: {
return_url: body.return_url || 'https://example.com/payment/status'
}
})
});
if (cfRes.ok) {
const cfData: any = await cfRes.json();
if (cfData.payment_session_id) {
cfSessionId = cfData.payment_session_id;
}
}
} catch (e) {
// fallback to generated payment session ID
}
await c.env.DB.prepare(
`INSERT INTO orders (order_id, user_email, amount, currency, status, payment_session_id, created_at, updated_at)
VALUES (?, ?, ?, 'INR', 'PENDING', ?, ?, ?)`
).bind(orderId, user.email || null, amount, cfSessionId, now, now).run();
return c.json({
success: true,
order_id: orderId,
payment_session_id: cfSessionId
});
});
publicRoutes.get('/orders/verify', authMiddleware, async (c) => {
const orderId = c.req.query('order_id');
if (!orderId) {
return c.json({ success: false, msg: "order_id query parameter is required" }, 400);
}
let order = await c.env.DB.prepare('SELECT * FROM orders WHERE order_id = ?').bind(orderId).first<any>();
if (!order) {
return c.json({ success: false, msg: "Order not found" }, 404);
}
let status = order.status;
try {
const cfRes = await fetch(`https://sandbox.cashfree.com/pg/orders/${orderId}`, {
method: 'GET',
headers: {
'x-api-version': '2022-09-01',
'x-client-id': (c.env as any).CASHFREE_APP_ID || 'sandbox_app_id',
'x-client-secret': (c.env as any).CASHFREE_SECRET_KEY || 'sandbox_secret'
}
});
if (cfRes.ok) {
const cfData: any = await cfRes.json();
if (cfData.order_status) {
status = cfData.order_status;
}
}
} catch (e) {
// preserve existing order status on error
}
const now = new Date().toISOString();
await c.env.DB.prepare('UPDATE orders SET status = ?, updated_at = ? WHERE order_id = ?').bind(status, now, orderId).run();
order = await c.env.DB.prepare('SELECT * FROM orders WHERE order_id = ?').bind(orderId).first();
return c.json({ success: true, status, order });
});
publicRoutes.get('/geocode', authMiddleware, async (c) => {
const q = c.req.query('q');
if (!q) {
return c.json({ results: [] });
}
try {
const res = await fetch(`https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=jsonv2&addressdetails=1&limit=5`, {
headers: { 'User-Agent': 'MWorld-Fullstack-Builder/1.0' }
});
const results = await res.json();
return c.json({ results: Array.isArray(results) ? results : [] });
} catch (e) {
return c.json({ results: [] });
}
});
publicRoutes.get('/reverse-geocode', authMiddleware, async (c) => {
const lat = c.req.query('lat');
const lon = c.req.query('lon');
if (!lat || !lon) {
return c.json({ result: null });
}
try {
const res = await fetch(`https://nominatim.openstreetmap.org/reverse?lat=${encodeURIComponent(lat)}&lon=${encodeURIComponent(lon)}&format=jsonv2&addressdetails=1`, {
headers: { 'User-Agent': 'MWorld-Fullstack-Builder/1.0' }
});
const result = await res.json();
return c.json({ result });
} catch (e) {
return c.json({ result: null });
}
});
export default publicRoutes;

237
backend/src/routes/utils.ts Normal file
View File

@ -0,0 +1,237 @@
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);
};

237
backend/src/utils.ts Normal file
View File

@ -0,0 +1,237 @@
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);
};

27
backend/wrangler.json Normal file
View File

@ -0,0 +1,27 @@
{
"name": "app-s01qyw",
"main": "src/index.ts",
"compatibility_date": "2024-04-01",
"d1_databases": [
{
"binding": "DB",
"database_name": "db-s01qyw",
"database_id": "7bc9cff6-4a3a-4a99-8d30-ac35203ec8fb"
}
],
"r2_buckets": [
{
"binding": "R2",
"bucket_name": "bucket-s01qyw"
}
],
"ai": {
"binding": "AI"
},
"vars": {
"S3_ENDPOINT": "",
"OSRM_URL": "https://router.project-osrm.org",
"BUSINESS_ID": "bi-4de464ad-b6bf-4848-8be7-a883910f0499Sep2026",
"SITE_ID": "s01qyw"
}
}