feat: add backend logic in /backend

This commit is contained in:
MWorld Deployer 2026-09-28 22:45:58 +05:30
parent 33f107374e
commit 296026831b
7 changed files with 1990 additions and 0 deletions

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

@ -0,0 +1,32 @@
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';
import { liveRoutes } from './routes/live-location';
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('/api', publicRoutes);
app.route('/api/admin', adminRoutes);
app.onError((err, c) => {
console.error('Unhandled Server Error:', err);
return c.json({ success: false, msg: err.message || 'Internal server error' }, 500);
});
app.route('/', agentic);
app.route('/api', liveRoutes);
export default app;

View File

@ -0,0 +1,88 @@
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<any>();
return c.json({ success: true, users: results || [] });
});
adminRoutes.get('/stats', async (c) => {
const totalUsersRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM users').first<{ c: number }>();
const activeRestRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM restaurants WHERE status = "active"').first<{ c: number }>();
const onlineDriversRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM delivery_agents WHERE is_available = 1').first<{ c: number }>();
const totalOrdersRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM orders').first<{ c: number }>();
const activeOrdersRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM orders WHERE status NOT IN ("delivered", "cancelled")').first<{ c: number }>();
const totalRevRes = await c.env.DB.prepare('SELECT SUM(total_amount) as s FROM orders WHERE status = "delivered"').first<{ s: number }>();
return c.json({
success: true,
stats: {
total_users: totalUsersRes?.c || 0,
active_restaurants: activeRestRes?.c || 0,
online_drivers: onlineDriversRes?.c || 0,
total_orders: totalOrdersRes?.c || 0,
active_orders: activeOrdersRes?.c || 0,
total_revenue: totalRevRes?.s || 0.0,
},
});
});
adminRoutes.get('/overview', async (c) => {
const totalUsersRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM users').first<{ c: number }>();
const activeRestRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM restaurants WHERE status = "active"').first<{ c: number }>();
const onlineDriversRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM delivery_agents WHERE is_available = 1').first<{ c: number }>();
const totalOrdersRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM orders').first<{ c: number }>();
const activeOrdersRes = await c.env.DB.prepare('SELECT COUNT(*) as c FROM orders WHERE status NOT IN ("delivered", "cancelled")').first<{ c: number }>();
const totalRevRes = await c.env.DB.prepare('SELECT SUM(total_amount) as s FROM orders WHERE status = "delivered"').first<{ s: number }>();
return c.json({
success: true,
stats: {
total_users: totalUsersRes?.c || 0,
active_restaurants: activeRestRes?.c || 0,
online_drivers: onlineDriversRes?.c || 0,
total_orders: totalOrdersRes?.c || 0,
active_orders: activeOrdersRes?.c || 0,
total_revenue: totalRevRes?.s || 0.0,
},
});
});
adminRoutes.get('/orders', async (c) => {
const { results } = await c.env.DB.prepare(
`SELECT o.*, r.name as restaurant_name
FROM orders o
LEFT JOIN restaurants r ON o.restaurant_id = r.id
ORDER BY o.created_at DESC`
).all<any>();
return c.json({ success: true, orders: 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,196 @@
import type { AppContext } from '../utils';
import { Hono } from 'hono';
import { authMiddleware } from '../utils';
type LiveRecord = {
id: string;
customerEmail?: string | null;
ownerEmail?: string | null;
agentEmail?: string | null;
originLat: number | null;
originLng: number | null;
destinationLat: number | null;
destinationLng: number | null;
};
type LivePosition = {
lat: number;
lng: number;
heading: number;
speed: number;
ts: number;
live_until: number;
};
export const liveRoutes = new Hono<AppContext>();
const livePositionStore = new Map<string, LivePosition>();
const routeCache = new Map<string, { expiresAt: number; value: any }>();
const POSITION_TTL_MS = 45_000;
const ROUTE_TTL_MS = 300_000;
function finiteCoordinate(value: unknown, min: number, max: number): number | null {
const numberValue = typeof value === 'number' ? value : Number(value);
return Number.isFinite(numberValue) && numberValue >= min && numberValue <= max ? numberValue : null;
}
async function queryFirst(c: any, sql: string, id: string): Promise<any | null> {
try {
return await c.env.DB.prepare(sql).bind(id).first();
} catch {
return null;
}
}
async function loadLiveRecord(c: any, id: string): Promise<LiveRecord | null> {
const foodOrder = await queryFirst(c,
'SELECT o.id, o.customer_email AS customer_email, o.delivery_agent_email AS agent_email, r.owner_email AS owner_email, r.latitude AS origin_lat, r.longitude AS origin_lng, o.delivery_latitude AS destination_lat, o.delivery_longitude AS destination_lng FROM orders o JOIN restaurants r ON r.id = o.restaurant_id WHERE o.id = ?', id);
if (foodOrder) return {
id: String(foodOrder.id), customerEmail: foodOrder.customer_email, ownerEmail: foodOrder.owner_email,
agentEmail: foodOrder.agent_email, originLat: finiteCoordinate(foodOrder.origin_lat, -90, 90),
originLng: finiteCoordinate(foodOrder.origin_lng, -180, 180), destinationLat: finiteCoordinate(foodOrder.destination_lat, -90, 90),
destinationLng: finiteCoordinate(foodOrder.destination_lng, -180, 180),
};
const localOrder = await queryFirst(c,
'SELECT o.id, o.buyer_email AS customer_email, o.rider_email AS agent_email, s.owner_email AS owner_email, s.latitude AS origin_lat, s.longitude AS origin_lng, o.delivery_latitude AS destination_lat, o.delivery_longitude AS destination_lng FROM local_orders o JOIN local_stores s ON s.id = o.store_id WHERE o.id = ?', id);
if (localOrder) return {
id: String(localOrder.id), customerEmail: localOrder.customer_email, ownerEmail: localOrder.owner_email,
agentEmail: localOrder.agent_email, originLat: finiteCoordinate(localOrder.origin_lat, -90, 90),
originLng: finiteCoordinate(localOrder.origin_lng, -180, 180), destinationLat: finiteCoordinate(localOrder.destination_lat, -90, 90),
destinationLng: finiteCoordinate(localOrder.destination_lng, -180, 180),
};
const trip = await queryFirst(c,
'SELECT id, driver_email AS agent_email, origin_lat, origin_lng, destination_lat, destination_lng FROM trips WHERE id = ?', id);
if (!trip) return null;
return {
id: String(trip.id), agentEmail: trip.agent_email, originLat: finiteCoordinate(trip.origin_lat, -90, 90),
originLng: finiteCoordinate(trip.origin_lng, -180, 180), destinationLat: finiteCoordinate(trip.destination_lat, -90, 90),
destinationLng: finiteCoordinate(trip.destination_lng, -180, 180),
};
}
async function isAuthorizedParticipant(c: any, record: LiveRecord, id: string): Promise<boolean> {
const email = String(c.get('user')?.email || '');
if (!email) return false;
if ([record.customerEmail, record.ownerEmail, record.agentEmail].filter(Boolean).includes(email)) return true;
try {
const participant = await c.env.DB.prepare('SELECT 1 FROM trip_participants WHERE trip_id = ? AND user_email = ? LIMIT 1').bind(id, email).first();
return Boolean(participant);
} catch {
return false;
}
}
async function routeHandler(c: any) {
const id = c.req.param('id');
const record = await loadLiveRecord(c, id);
if (!record) return c.json({ success: false, msg: 'Order or trip not found' }, 404);
if (!(await isAuthorizedParticipant(c, record, id))) return c.json({ success: false, msg: 'Not allowed to view this route' }, 403);
if ([record.originLat, record.originLng, record.destinationLat, record.destinationLng].some(value => value === null)) {
return c.json({ success: false, msg: 'Pickup and delivery coordinates are required' }, 422);
}
const cacheKey = [record.originLat, record.originLng, record.destinationLat, record.destinationLng].join(':');
const cached = routeCache.get(cacheKey);
if (cached && cached.expiresAt > Date.now()) return c.json({ success: true, route: cached.value, ...cached.value });
const baseUrl = String(c.env.OSRM_URL || 'https://router.project-osrm.org').replace(/\/$/, '');
const endpoint = baseUrl + '/route/v1/driving/' + record.originLng + ',' + record.originLat + ';' + record.destinationLng + ',' + record.destinationLat + '?overview=full&geometries=geojson&steps=true';
const providerResponse = await fetch(endpoint);
const providerData = await providerResponse.json().catch(() => null) as any;
const providerRoute = providerData?.routes?.[0];
if (!providerResponse.ok || !providerRoute?.geometry?.coordinates) {
return c.json({ success: false, msg: 'Road route is temporarily unavailable' }, 502);
}
const route = {
geometry: providerRoute.geometry,
distance_m: Number(providerRoute.distance),
duration_s: Number(providerRoute.duration),
steps: Array.isArray(providerRoute.legs) ? providerRoute.legs.flatMap((leg: any) => Array.isArray(leg?.steps) ? leg.steps : []) : [],
served_by: 'osrm',
};
routeCache.set(cacheKey, { expiresAt: Date.now() + ROUTE_TTL_MS, value: route });
return c.json({ success: true, route, ...route });
}
async function persistPosition(c: any, email: string, position: LivePosition): Promise<void> {
for (const table of ['delivery_agents', 'delivery_riders']) {
try {
const result = await c.env.DB.prepare('UPDATE ' + table + ' SET current_latitude = ?, current_longitude = ?, updated_at = ? WHERE user_email = ?')
.bind(position.lat, position.lng, new Date(position.ts).toISOString(), email).run();
if (Number(result?.meta?.changes || 0) > 0) return;
} catch {
// The generated contract may use the other delivery-participant table.
}
}
}
async function publishPosition(c: any) {
const user = c.get('user') || {};
if (!['delivery_agent', 'delivery_rider', 'driver'].includes(String(user.role || ''))) {
return c.json({ success: false, msg: 'Only an active delivery participant can share location' }, 403);
}
const id = c.req.param('id');
if (id) {
const record = await loadLiveRecord(c, id);
if (!record) return c.json({ success: false, msg: 'Order or trip not found' }, 404);
if (String(record.agentEmail || '') !== String(user.email || '')) {
return c.json({ success: false, msg: 'Only the assigned delivery participant can share this position' }, 403);
}
}
const body = await c.req.json().catch(() => ({}));
const lat = finiteCoordinate(body.latitude ?? body.lat, -90, 90);
const lng = finiteCoordinate(body.longitude ?? body.lng, -180, 180);
const heading = finiteCoordinate(body.heading ?? 0, 0, 360);
const speed = finiteCoordinate(body.speed ?? 0, 0, Number.MAX_SAFE_INTEGER);
if (lat === null || lng === null || heading === null || speed === null) {
return c.json({ success: false, msg: 'Location data is invalid' }, 422);
}
const ts = Number(body.ts || Date.now());
if (!Number.isFinite(ts) || ts < Date.now() - 120_000 || ts > Date.now() + 30_000) {
return c.json({ success: false, msg: 'Location update is stale or invalid' }, 422);
}
const position = { lat, lng, heading, speed, ts, live_until: Date.now() + POSITION_TTL_MS };
const key = id ? 'order:' + id : 'agent:' + String(user.email || '');
livePositionStore.set(key, position);
await persistPosition(c, String(user.email || ''), position);
return c.json({ success: true, position, state: 'live' });
}
async function readPosition(c: any) {
const id = c.req.param('id');
const record = await loadLiveRecord(c, id);
if (!record) return c.json({ success: false, msg: 'Order or trip not found' }, 404);
if (!(await isAuthorizedParticipant(c, record, id))) return c.json({ success: false, msg: 'Not allowed to view this position' }, 403);
const memoryPosition = livePositionStore.get('order:' + id);
let position = memoryPosition;
if (!position && record.agentEmail) {
for (const table of ['delivery_agents', 'delivery_riders']) {
try {
const row = await c.env.DB.prepare('SELECT current_latitude, current_longitude, updated_at FROM ' + table + ' WHERE user_email = ?').bind(record.agentEmail).first<any>();
const lat = finiteCoordinate(row?.current_latitude, -90, 90);
const lng = finiteCoordinate(row?.current_longitude, -180, 180);
const updatedAt = Date.parse(String(row?.updated_at || ''));
if (lat !== null && lng !== null && Number.isFinite(updatedAt)) {
position = { lat, lng, heading: 0, speed: 0, ts: updatedAt, live_until: updatedAt + POSITION_TTL_MS };
break;
}
} catch {
// The generated contract may use the other delivery-participant table.
}
}
}
const live = Boolean(position && position.live_until > Date.now());
return c.json({ success: true, position: live ? position : null, state: position ? (live ? 'live' : 'expired') : 'unavailable', live_until: position?.live_until || null });
}
liveRoutes.get('/orders/:id/route', authMiddleware, routeHandler);
liveRoutes.get('/trips/:id/route', authMiddleware, routeHandler);
liveRoutes.get('/orders/:id/position', authMiddleware, readPosition);
liveRoutes.get('/trips/:id/position', authMiddleware, readPosition);
liveRoutes.post('/orders/:id/position', authMiddleware, publishPosition);
liveRoutes.post('/trips/:id/position', authMiddleware, publishPosition);
liveRoutes.patch('/delivery/location', authMiddleware, publishPosition);
liveRoutes.patch('/rider/location', authMiddleware, publishPosition);

View File

@ -0,0 +1,995 @@
import { Hono } from 'hono';
import { AppContext, authMiddleware, verifyMAuthToken, getMWorldOrigin } from '../utils';
const publicRoutes = new Hono<AppContext>();
// -----------------------------------------------------------------------------
// INFRASTRUCTURE & AUTHENTICATION
// -----------------------------------------------------------------------------
function __requireRole(allowedRoles: string[]) {
return async (c: any, next: any) => {
const authenticatedUser = c.get('user') as { email?: string } | undefined;
const email = authenticatedUser?.email;
if (!email) return c.json({ success: false, msg: 'Unauthorized' }, 401);
const persistedUser = await c.env.DB.prepare('SELECT role FROM users WHERE email = ?')
.bind(email || null)
.first<{ role?: string | null }>();
const role = String(persistedUser?.role || '');
if (!allowedRoles.includes(role)) {
return c.json({ success: false, msg: 'You do not have permission for this action' }, 403);
}
await next();
};
}
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');
try {
const res = await c.env.DB.prepare(
'SELECT COUNT(*) as c FROM notifications WHERE recipient_email = ? AND is_read = 0'
)
.bind(user.email || null)
.first<{ c: number }>();
return c.json({ count: res?.c || 0 });
} catch {
return c.json({ count: 0 });
}
});
publicRoutes.post('/user/check-in', async (c) => {
try {
const body = await c.req.json().catch(() => ({}));
const token = body.mauth_token || body.user_access_token;
if (!token) {
return c.json({ success: false, msg: 'Missing authentication token' }, 401);
}
const origin = getMWorldOrigin(c);
const mauthUser = await verifyMAuthToken(token, origin);
if (!mauthUser || !mauthUser.email) {
return c.json({ success: false, msg: 'Invalid authentication token' }, 401);
}
const existing = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?')
.bind(mauthUser.email)
.first<any>();
const fullName = mauthUser.fullName || mauthUser.full_name || 'User';
const phone = mauthUser.phone || null;
const citizenId = mauthUser.citizen_id || null;
const username =
existing?.username ||
mauthUser.username ||
`${mauthUser.email.split('@')[0]}_${Math.floor(1000 + Math.random() * 9000)}`;
const role = existing?.role || null;
if (existing) {
await c.env.DB.prepare(
`UPDATE users SET full_name = ?, phone = ?, citizen_id = ?, mauth_token = ?, username = ?, updated_at = datetime('now') WHERE email = ?`
)
.bind(fullName, phone, citizenId, token, username, mauthUser.email)
.run();
} else {
await c.env.DB.prepare(
`INSERT INTO users (email, full_name, phone, role, username, mauth_token, citizen_id, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`
)
.bind(mauthUser.email, fullName, phone, role, username, token, citizenId)
.run();
}
const updatedUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?')
.bind(mauthUser.email)
.first<any>();
return c.json({ success: true, user: updatedUser });
} catch (err: any) {
return c.json({ success: false, msg: err.message || 'Authentication failed' }, 401);
}
});
publicRoutes.get('/users/me', authMiddleware, async (c) => {
const user = c.get('user');
const dbUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?')
.bind(user.email || null)
.first<any>();
if (!dbUser) {
return c.json({ success: false, msg: 'User profile not found' }, 404);
}
return c.json({ success: true, user: dbUser });
});
publicRoutes.patch('/users/me/role', authMiddleware, async (c) => {
const user = c.get('user');
const body = await c.req.json().catch(() => ({}));
const { role } = body;
const ALLOWED_ROLES = ['customer', 'restaurant', 'delivery_agent'];
if (!role || !ALLOWED_ROLES.includes(role)) {
return c.json({ success: false, msg: 'Role must be one of: customer, restaurant, delivery_agent' }, 400);
}
await c.env.DB.prepare(`UPDATE users SET role = ?, updated_at = datetime('now') WHERE email = ?`)
.bind(role, user.email || null)
.run();
if (role === 'delivery_agent') {
const existingAgent = await c.env.DB.prepare('SELECT * FROM delivery_agents WHERE user_email = ?')
.bind(user.email || null)
.first();
if (!existingAgent) {
await c.env.DB.prepare(
`INSERT INTO delivery_agents (user_email, vehicle_type, is_available, updated_at) VALUES (?, 'Bike', 1, datetime('now'))`
)
.bind(user.email || null)
.run();
}
}
const updatedUser = await c.env.DB.prepare('SELECT * FROM users WHERE email = ?')
.bind(user.email || null)
.first<any>();
return c.json({ success: true, user: updatedUser });
});
// -----------------------------------------------------------------------------
// GEOCODING & MAP PROXIES
// -----------------------------------------------------------------------------
publicRoutes.get('/geocode', authMiddleware, async (c) => {
const q = c.req.query('q');
if (!q) return c.json({ results: [] });
try {
const url = `https://nominatim.openstreetmap.org/search?q=${encodeURIComponent(q)}&format=jsonv2&addressdetails=1&limit=5`;
const res = await fetch(url, {
headers: { 'User-Agent': 'MWorld-FoodDelivery/1.0' },
});
const data = await res.json();
return c.json({ results: data });
} catch {
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 url = `https://nominatim.openstreetmap.org/reverse?lat=${lat}&lon=${lon}&format=jsonv2&addressdetails=1`;
const res = await fetch(url, {
headers: { 'User-Agent': 'MWorld-FoodDelivery/1.0' },
});
const data = await res.json();
return c.json({ result: data });
} catch {
return c.json({ result: null });
}
});
// -----------------------------------------------------------------------------
// R2 FILE UPLOADS & MEDIA PROXY
// -----------------------------------------------------------------------------
publicRoutes.post('/upload', authMiddleware, async (c) => {
try {
const body = await c.req.parseBody();
const file = body['file'];
if (!file || !(file instanceof File)) {
return c.json({ success: false, msg: 'Valid file is required' }, 400);
}
const fileExt = file.name.split('.').pop() || 'bin';
const key = `${crypto.randomUUID()}.${fileExt}`;
const fileData = await file.arrayBuffer();
await c.env.R2.put(key, fileData, {
httpMetadata: { contentType: file.type || 'application/octet-stream' },
});
return c.json({ success: true, url: `/api/media/${key}` });
} catch (err: any) {
return c.json({ success: false, msg: err.message || 'Upload failed' }, 500);
}
});
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 });
})
// -----------------------------------------------------------------------------
// RESTAURANT MANAGEMENT & DISCOVERY
// -----------------------------------------------------------------------------
publicRoutes.get('/restaurants/me/orders', authMiddleware, __requireRole(["restaurant"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'restaurant') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const store = await c.env.DB.prepare('SELECT id FROM restaurants WHERE owner_email = ?')
.bind(user.email || null)
.first<{ id: string }>();
if (!store) {
return c.json({ success: true, orders: [] });
}
const { results } = await c.env.DB.prepare(
'SELECT * FROM orders WHERE restaurant_id = ? ORDER BY created_at DESC'
)
.bind(store.id)
.all<any>();
return c.json({ success: true, orders: results || [] });
});
publicRoutes.patch('/restaurants/me/orders/:id/status', authMiddleware, __requireRole(["restaurant"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'restaurant') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const orderId = c.req.param('id');
const body = await c.req.json().catch(() => ({}));
const { status } = body;
const ALLOWED_STATUSES = ['accepted', 'preparing', 'ready_for_pickup', 'cancelled'];
if (!status || !ALLOWED_STATUSES.includes(status)) {
return c.json({ success: false, msg: `Status must be one of: ${ALLOWED_STATUSES.join(', ')}` }, 400);
}
const store = await c.env.DB.prepare('SELECT id FROM restaurants WHERE owner_email = ?')
.bind(user.email || null)
.first<{ id: string }>();
if (!store) {
return c.json({ success: false, msg: 'Restaurant profile not found' }, 404);
}
const order = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ? AND restaurant_id = ?')
.bind(orderId, store.id)
.first<any>();
if (!order) {
return c.json({ success: false, msg: 'Order not found for this restaurant' }, 404);
}
await c.env.DB.prepare(`UPDATE orders SET status = ?, updated_at = datetime('now') WHERE id = ?`)
.bind(status, orderId)
.run();
const updatedOrder = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?')
.bind(orderId)
.first<any>();
return c.json({ success: true, order: updatedOrder });
});
publicRoutes.get('/restaurants/me', authMiddleware, __requireRole(["restaurant"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'restaurant') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const store = await c.env.DB.prepare('SELECT * FROM restaurants WHERE owner_email = ?')
.bind(user.email || null)
.first<any>();
return c.json({ success: true, restaurant: store || null });
});
publicRoutes.get('/restaurants', async (c) => {
const search = c.req.query('search')?.trim();
let query = 'SELECT * FROM restaurants WHERE status = "active"';
const binds: any[] = [];
if (search) {
query += ' AND (name LIKE ? OR description LIKE ? OR address LIKE ?)';
const term = `%${search}%`;
binds.push(term, term, term);
}
query += ' ORDER BY created_at DESC';
const stmt = c.env.DB.prepare(query);
const { results } = binds.length ? await stmt.bind(...binds).all<any>() : await stmt.all<any>();
return c.json({ success: true, restaurants: results || [] });
});
publicRoutes.get('/restaurants/:id/menu-items', async (c) => {
const restaurantId = c.req.param('id');
const { results } = await c.env.DB.prepare(
'SELECT * FROM menu_items WHERE restaurant_id = ? AND is_available = 1 ORDER BY category ASC, name ASC'
)
.bind(restaurantId)
.all<any>();
return c.json({ success: true, menu_items: results || [] });
});
publicRoutes.get('/restaurants/:id', async (c) => {
const id = c.req.param('id');
const store = await c.env.DB.prepare('SELECT * FROM restaurants WHERE id = ?')
.bind(id)
.first<any>();
if (!store) {
return c.json({ success: false, msg: 'Restaurant not found' }, 404);
}
return c.json({ success: true, restaurant: store });
});
publicRoutes.post('/restaurants', authMiddleware, __requireRole(["restaurant"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'restaurant') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const existing = await c.env.DB.prepare('SELECT * FROM restaurants WHERE owner_email = ?')
.bind(user.email || null)
.first<any>();
if (existing) {
return c.json({ success: false, msg: 'Restaurant already exists for this account' }, 400);
}
const body = await c.req.json().catch(() => ({}));
const { name, description, address, latitude, longitude, image_url } = body;
if (!name || !address || latitude === undefined || longitude === undefined) {
return c.json({ success: false, msg: 'Name, address, latitude, and longitude are required' }, 400);
}
const id = crypto.randomUUID();
await c.env.DB.prepare(
`INSERT INTO restaurants (id, owner_email, name, description, address, latitude, longitude, image_url, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'active', datetime('now'), datetime('now'))`
)
.bind(id, user.email || null, name, description || null, address, Number(latitude),
Number(longitude),
image_url || null
)
.run();
const newStore = await c.env.DB.prepare('SELECT * FROM restaurants WHERE id = ?')
.bind(id)
.first<any>();
return c.json({ success: true, restaurant: newStore });
});
publicRoutes.patch('/restaurants/:id', authMiddleware, __requireRole(["restaurant"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'restaurant') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const id = c.req.param('id');
const store = await c.env.DB.prepare('SELECT * FROM restaurants WHERE id = ? AND owner_email = ?')
.bind(id, user.email || null)
.first<any>();
if (!store) {
return c.json({ success: false, msg: 'Restaurant not found or unauthorized' }, 404);
}
const body = await c.req.json().catch(() => ({}));
const name = body.name !== undefined ? body.name : store.name;
const description = body.description !== undefined ? body.description : store.description;
const address = body.address !== undefined ? body.address : store.address;
const latitude = body.latitude !== undefined ? Number(body.latitude) : store.latitude;
const longitude = body.longitude !== undefined ? Number(body.longitude) : store.longitude;
const image_url = body.image_url !== undefined ? body.image_url : store.image_url;
const status = body.status !== undefined ? body.status : store.status;
if (status && !['active', 'closed'].includes(status)) {
return c.json({ success: false, msg: 'Status must be active or closed' }, 400);
}
await c.env.DB.prepare(
`UPDATE restaurants SET name = ?, description = ?, address = ?, latitude = ?, longitude = ?, image_url = ?, status = ?, updated_at = datetime('now') WHERE id = ?`
)
.bind(name, description || null, address, latitude, longitude, image_url || null, status, id)
.run();
const updated = await c.env.DB.prepare('SELECT * FROM restaurants WHERE id = ?')
.bind(id)
.first<any>();
return c.json({ success: true, restaurant: updated });
});
// -----------------------------------------------------------------------------
// MENU ITEM MANAGEMENT
// -----------------------------------------------------------------------------
publicRoutes.post('/menu-items', authMiddleware, __requireRole(["restaurant"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'restaurant') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const body = await c.req.json().catch(() => ({}));
const { restaurant_id, name, description, category, ingredients, recipe, price, image_url, is_available } = body;
if (!restaurant_id || !name || price === undefined) {
return c.json({ success: false, msg: 'restaurant_id, name, and price are required' }, 400);
}
const store = await c.env.DB.prepare('SELECT * FROM restaurants WHERE id = ? AND owner_email = ?')
.bind(restaurant_id, user.email || null)
.first<any>();
if (!store) {
return c.json({ success: false, msg: 'Restaurant not found or unauthorized' }, 403);
}
const id = crypto.randomUUID();
const availableVal = is_available !== undefined ? (is_available ? 1 : 0) : 1;
await c.env.DB.prepare(
`INSERT INTO menu_items (id, restaurant_id, name, description, category, ingredients, recipe, price, image_url, is_available, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))`
)
.bind(id, restaurant_id, name, description || null, category || null, ingredients || null, recipe || null, Number(price),
image_url || null,
availableVal
)
.run();
const item = await c.env.DB.prepare('SELECT * FROM menu_items WHERE id = ?')
.bind(id)
.first<any>();
return c.json({ success: true, menu_item: item });
});
publicRoutes.patch('/menu-items/:id', authMiddleware, __requireRole(["restaurant"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'restaurant') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const id = c.req.param('id');
const item = await c.env.DB.prepare(
`SELECT m.* FROM menu_items m JOIN restaurants r ON m.restaurant_id = r.id WHERE m.id = ? AND r.owner_email = ?`
)
.bind(id, user.email || null)
.first<any>();
if (!item) {
return c.json({ success: false, msg: 'Menu item not found or unauthorized' }, 404);
}
const body = await c.req.json().catch(() => ({}));
const name = body.name !== undefined ? body.name : item.name;
const description = body.description !== undefined ? body.description : item.description;
const category = body.category !== undefined ? body.category : item.category;
const ingredients = body.ingredients !== undefined ? body.ingredients : item.ingredients;
const recipe = body.recipe !== undefined ? body.recipe : item.recipe;
const price = body.price !== undefined ? Number(body.price) : item.price;
const image_url = body.image_url !== undefined ? body.image_url : item.image_url;
const is_available = body.is_available !== undefined ? (body.is_available ? 1 : 0) : item.is_available;
await c.env.DB.prepare(
`UPDATE menu_items SET name = ?, description = ?, category = ?, ingredients = ?, recipe = ?, price = ?, image_url = ?, is_available = ?, updated_at = datetime('now') WHERE id = ?`
)
.bind(name, description || null, category || null, ingredients || null, recipe || null, price, image_url || null, is_available, id)
.run();
const updated = await c.env.DB.prepare('SELECT * FROM menu_items WHERE id = ?')
.bind(id)
.first<any>();
return c.json({ success: true, menu_item: updated });
});
publicRoutes.delete('/menu-items/:id', authMiddleware, __requireRole(["restaurant"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'restaurant') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const id = c.req.param('id');
const item = await c.env.DB.prepare(
`SELECT m.id FROM menu_items m JOIN restaurants r ON m.restaurant_id = r.id WHERE m.id = ? AND r.owner_email = ?`
)
.bind(id, user.email || null)
.first<any>();
if (!item) {
return c.json({ success: false, msg: 'Menu item not found or unauthorized' }, 404);
}
await c.env.DB.prepare('DELETE FROM menu_items WHERE id = ?').bind(id).run();
return c.json({ success: true });
});
// -----------------------------------------------------------------------------
// DELIVERY PARTNER OPERATIONS
// -----------------------------------------------------------------------------
publicRoutes.patch('/delivery/profile', authMiddleware, __requireRole(["delivery_agent"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'delivery_agent') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const body = await c.req.json().catch(() => ({}));
const { vehicle_type, is_available } = body;
const agent = await c.env.DB.prepare('SELECT * FROM delivery_agents WHERE user_email = ?')
.bind(user.email || null)
.first<any>();
const vehicleVal = vehicle_type !== undefined ? vehicle_type : agent?.vehicle_type || 'Bike';
const availVal = is_available !== undefined ? (is_available ? 1 : 0) : agent?.is_available ?? 1;
if (agent) {
await c.env.DB.prepare(
`UPDATE delivery_agents SET vehicle_type = ?, is_available = ?, updated_at = datetime('now') WHERE user_email = ?`
)
.bind(vehicleVal, availVal, user.email || null)
.run();
} else {
await c.env.DB.prepare(
`INSERT INTO delivery_agents (user_email, vehicle_type, is_available, updated_at) VALUES (?, ?, ?, datetime('now'))`
)
.bind(user.email || null, vehicleVal, availVal)
.run();
}
const updated = await c.env.DB.prepare('SELECT * FROM delivery_agents WHERE user_email = ?')
.bind(user.email || null)
.first<any>();
return c.json({ success: true, delivery_agent: updated });
});
publicRoutes.get('/delivery/orders/available', authMiddleware, __requireRole(["delivery_agent"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'delivery_agent') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const { results } = await c.env.DB.prepare(
`SELECT o.*, r.name as restaurant_name, r.address as restaurant_address, r.latitude as restaurant_latitude, r.longitude as restaurant_longitude
FROM orders o
JOIN restaurants r ON o.restaurant_id = r.id
WHERE o.status = 'ready_for_pickup' AND o.delivery_agent_email IS NULL
ORDER BY o.created_at ASC`
).all<any>();
return c.json({ success: true, orders: results || [] });
});
publicRoutes.get('/delivery/orders/active', authMiddleware, __requireRole(["delivery_agent"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'delivery_agent') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const { results } = await c.env.DB.prepare(
`SELECT o.*, r.name as restaurant_name, r.address as restaurant_address, r.latitude as restaurant_latitude, r.longitude as restaurant_longitude
FROM orders o
JOIN restaurants r ON o.restaurant_id = r.id
WHERE o.delivery_agent_email = ? AND o.status IN ('assigned', 'picked_up')
ORDER BY o.created_at DESC`
)
.bind(user.email || null)
.all<any>();
return c.json({ success: true, orders: results || [] });
});
publicRoutes.post('/delivery/orders/:id/claim', authMiddleware, __requireRole(["delivery_agent"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'delivery_agent') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const orderId = c.req.param('id');
const activeOrder = await c.env.DB.prepare(
`SELECT id FROM orders WHERE delivery_agent_email = ? AND status IN ('assigned', 'picked_up')`
)
.bind(user.email || null)
.first<any>();
if (activeOrder) {
return c.json({ success: false, msg: 'You already have an active delivery in progress' }, 400);
}
const res = await c.env.DB.prepare(
`UPDATE orders SET delivery_agent_email = ?, status = 'assigned', updated_at = datetime('now') WHERE id = ? AND status = 'ready_for_pickup' AND delivery_agent_email IS NULL`
)
.bind(user.email || null, orderId)
.run();
if (!res.success || res.meta.changes === 0) {
return c.json({ success: false, msg: 'Order was already claimed by another driver or is unavailable' }, 409);
}
const updatedOrder = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?')
.bind(orderId)
.first<any>();
return c.json({ success: true, order: updatedOrder });
});
publicRoutes.patch('/delivery/orders/:id/status', authMiddleware, __requireRole(["delivery_agent"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'delivery_agent') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const orderId = c.req.param('id');
const body = await c.req.json().catch(() => ({}));
const { status } = body;
if (!status || !['picked_up', 'delivered'].includes(status)) {
return c.json({ success: false, msg: 'Status must be picked_up or delivered' }, 400);
}
const order = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ? AND delivery_agent_email = ?')
.bind(orderId, user.email || null)
.first<any>();
if (!order) {
return c.json({ success: false, msg: 'Order not found or not assigned to you' }, 404);
}
await c.env.DB.prepare(`UPDATE orders SET status = ?, updated_at = datetime('now') WHERE id = ?`)
.bind(status, orderId)
.run();
const updatedOrder = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?')
.bind(orderId)
.first<any>();
return c.json({ success: true, order: updatedOrder });
});
// -----------------------------------------------------------------------------
// CUSTOMER ORDERS & TRACKING
// -----------------------------------------------------------------------------
publicRoutes.get('/orders/my', authMiddleware, __requireRole(["customer"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'customer') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const { results } = await c.env.DB.prepare(
`SELECT o.*, r.name as restaurant_name, r.image_url as restaurant_image
FROM orders o
JOIN restaurants r ON o.restaurant_id = r.id
WHERE o.customer_email = ?
ORDER BY o.created_at DESC`
)
.bind(user.email || null)
.all<any>();
return c.json({ success: true, orders: results || [] });
});
publicRoutes.post('/orders', authMiddleware, __requireRole(["customer"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'customer') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const body = await c.req.json().catch(() => ({}));
const { restaurant_id, delivery_address, delivery_latitude, delivery_longitude, items, return_url } = body;
if (!restaurant_id || !delivery_address || delivery_latitude === undefined || delivery_longitude === undefined || !Array.isArray(items) || items.length === 0) {
return c.json({ success: false, msg: 'restaurant_id, delivery address, coordinates, and items are required' }, 400);
}
const store = await c.env.DB.prepare('SELECT * FROM restaurants WHERE id = ? AND status = "active"')
.bind(restaurant_id)
.first<any>();
if (!store) {
return c.json({ success: false, msg: 'Restaurant is not available or closed' }, 400);
}
let subtotal = 0;
const verifiedItems: Array<{ id: string; name: string; price: number; quantity: number; special_instructions: string | null }> = [];
for (const item of items) {
if (!item.menu_item_id || !item.quantity || Number(item.quantity) <= 0) {
return c.json({ success: false, msg: 'Invalid item parameters' }, 400);
}
const menuItem = await c.env.DB.prepare('SELECT * FROM menu_items WHERE id = ? AND restaurant_id = ? AND is_available = 1')
.bind(item.menu_item_id, restaurant_id)
.first<any>();
if (!menuItem) {
return c.json({ success: false, msg: `Menu item ${item.menu_item_id} is unavailable` }, 400);
}
const qty = Math.floor(Number(item.quantity));
const itemTotal = menuItem.price * qty;
subtotal += itemTotal;
verifiedItems.push({
id: menuItem.id,
name: menuItem.name,
price: menuItem.price,
quantity: qty,
special_instructions: item.special_instructions || null,
});
}
const deliveryFee = 5.0;
const totalAmount = subtotal + deliveryFee;
const orderId = crypto.randomUUID();
let paymentSessionId = `session_mock_${crypto.randomUUID()}`;
try {
const appIdRow = await c.env.DB.prepare("SELECT value FROM settings WHERE key = 'cashfree_app_id'").first<{ value: string }>();
const secretKeyRow = await c.env.DB.prepare("SELECT value FROM settings WHERE key = 'cashfree_secret_key'").first<{ value: string }>();
const envRow = await c.env.DB.prepare("SELECT value FROM settings WHERE key = 'payment_env'").first<{ value: string }>();
if (appIdRow?.value && secretKeyRow?.value) {
const paymentEnv = envRow?.value === 'production' ? 'api' : 'sandbox';
const sanitizeCustomerId = user.email.replace(/[^a-zA-Z0-9]/g, '_');
const cfRes = await fetch(`https://${paymentEnv}.cashfree.com/pg/orders`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-client-id': appIdRow.value,
'x-client-secret': secretKeyRow.value,
'x-api-version': '2023-08-01',
},
body: JSON.stringify({
order_id: orderId,
order_amount: totalAmount,
order_currency: 'INR',
customer_details: {
customer_id: sanitizeCustomerId,
customer_email: user.email,
customer_phone: user.phone || '9999999999',
},
order_meta: {
return_url: return_url || `https://${getMWorldOrigin(c)}/app/orders`,
},
}),
});
const cfData: any = await cfRes.json();
if (cfData?.payment_session_id) {
paymentSessionId = cfData.payment_session_id;
}
}
} catch (err) {
console.error('Cashfree order creation fallback:', err);
}
const statements: any[] = [];
statements.push(
c.env.DB.prepare(
`INSERT INTO orders (id, customer_email, restaurant_id, delivery_address, delivery_latitude, delivery_longitude, subtotal, delivery_fee, total_amount, status, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 'placed', datetime('now'), datetime('now'))`
).bind(orderId, user.email || null, restaurant_id, delivery_address, Number(delivery_latitude),
Number(delivery_longitude),
subtotal,
deliveryFee,
totalAmount
)
);
for (const vi of verifiedItems) {
const orderItemId = crypto.randomUUID();
statements.push(
c.env.DB.prepare(
`INSERT INTO order_items (id, order_id, menu_item_id, item_name, quantity, unit_price, special_instructions)
VALUES (?, ?, ?, ?, ?, ?, ?)`
).bind(orderItemId, orderId, vi.id, vi.name, vi.quantity, vi.price, vi.special_instructions)
);
}
await c.env.DB.batch(statements);
const newOrder = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?').bind(orderId).first<any>();
const { results: newItems } = await c.env.DB.prepare('SELECT * FROM order_items WHERE order_id = ?').bind(orderId).all<any>();
return c.json({
success: true,
order: { ...newOrder, items: newItems || [] },
payment_session_id: paymentSessionId,
});
});
publicRoutes.patch('/orders/:id/cancel', authMiddleware, __requireRole(["customer"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'customer') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const orderId = c.req.param('id');
const order = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ? AND customer_email = ?')
.bind(orderId, user.email || null)
.first<any>();
if (!order) {
return c.json({ success: false, msg: 'Order not found' }, 404);
}
if (!['placed', 'accepted'].includes(order.status)) {
return c.json({ success: false, msg: 'Order cannot be cancelled once preparation has started' }, 400);
}
await c.env.DB.prepare(`UPDATE orders SET status = 'cancelled', updated_at = datetime('now') WHERE id = ?`)
.bind(orderId)
.run();
const updated = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?')
.bind(orderId)
.first<any>();
return c.json({ success: true, order: updated });
});
publicRoutes.post('/orders/:id/rating', authMiddleware, __requireRole(["customer"]), async (c) => {
const user = c.get('user');
if (user.role && user.role !== 'customer') {
return c.json({ success: false, msg: 'Forbidden' }, 403);
}
const orderId = c.req.param('id');
const order = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ? AND customer_email = ?')
.bind(orderId, user.email || null)
.first<any>();
if (!order) {
return c.json({ success: false, msg: 'Order not found' }, 404);
}
if (order.status !== 'delivered') {
return c.json({ success: false, msg: 'Ratings can only be submitted for delivered orders' }, 400);
}
const existingRating = await c.env.DB.prepare('SELECT * FROM ratings WHERE order_id = ?').bind(orderId).first();
if (existingRating) {
return c.json({ success: false, msg: 'Order has already been rated' }, 400);
}
const body = await c.req.json().catch(() => ({}));
const { restaurant_rating, driver_rating, comment } = body;
const rRating = Math.floor(Number(restaurant_rating));
if (!rRating || rRating < 1 || rRating > 5) {
return c.json({ success: false, msg: 'Restaurant rating must be an integer between 1 and 5' }, 400);
}
let dRating: number | null = null;
if (driver_rating !== undefined && driver_rating !== null) {
dRating = Math.floor(Number(driver_rating));
if (dRating < 1 || dRating > 5) {
return c.json({ success: false, msg: 'Driver rating must be an integer between 1 and 5' }, 400);
}
}
const ratingId = crypto.randomUUID();
await c.env.DB.prepare(
`INSERT INTO ratings (id, order_id, customer_email, restaurant_rating, driver_rating, comment, created_at)
VALUES (?, ?, ?, ?, ?, ?, datetime('now'))`
)
.bind(ratingId, orderId, user.email || null, rRating, dRating, comment || null)
.run();
const newRating = await c.env.DB.prepare('SELECT * FROM ratings WHERE id = ?').bind(ratingId).first<any>();
return c.json({ success: true, rating: newRating });
});
publicRoutes.get('/orders/:id', authMiddleware, async (c) => {
const user = c.get('user');
const orderId = c.req.param('id');
const order = await c.env.DB.prepare('SELECT * FROM orders WHERE id = ?')
.bind(orderId)
.first<any>();
if (!order) {
return c.json({ success: false, msg: 'Order not found' }, 404);
}
const store = await c.env.DB.prepare('SELECT * FROM restaurants WHERE id = ?')
.bind(order.restaurant_id)
.first<any>();
if (
order.customer_email !== user.email &&
order.delivery_agent_email !== user.email &&
store?.owner_email !== user.email
) {
return c.json({ success: false, msg: 'Unauthorized to view this order' }, 403);
}
const { results: items } = await c.env.DB.prepare('SELECT * FROM order_items WHERE order_id = ?')
.bind(orderId)
.all<any>();
let driverTracking = null;
if (order.delivery_agent_email) {
const agent = await c.env.DB.prepare('SELECT * FROM delivery_agents WHERE user_email = ?')
.bind(order.delivery_agent_email)
.first<any>();
if (agent) {
driverTracking = {
vehicle_type: agent.vehicle_type,
current_latitude: agent.current_latitude,
current_longitude: agent.current_longitude,
updated_at: agent.updated_at,
};
}
}
return c.json({
success: true,
order: {
...order,
restaurant_name: store?.name,
restaurant_address: store?.address,
restaurant_latitude: store?.latitude,
restaurant_longitude: store?.longitude,
items: items || [],
driver_tracking: driverTracking,
},
});
});
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);
};