/**
 * src/auth/middleware.ts
 * وسيط المصادقة — التحقق من هوية المستخدم وصلاحيات المستأجر
 */
import { existsSync, readFileSync } from 'node:fs';
import { join } from 'node:path';
import { timingSafeEqual } from 'node:crypto';
import { createChildLogger } from '../utils/logger.js';
import { config } from '../config.js';
import { supabase } from '../utils/supabase.js';
import { getDataDir } from '../utils/data-dir.js';
import { getTenantById } from '../tenants/manager.js';
const log = createChildLogger('auth:middleware');
// ─── Device-mode auth ────────────────────────────────────────────────
// Phase 1 device mode: a customer device runs Muassis for one business owner
// who is physically at the machine. The original v1.0 design accepted ANY
// non-empty bearer token (sentinel string "device") because the gateway
// was bound to 0.0.0.0 and "presence on the LAN" was treated as auth — a
// genuine vulnerability flagged in the v1.1 security audit.
//
// v1.1 lockdown:
//   1. HOST defaults to 127.0.0.1 (config.ts) so only the device owner can reach the API.
//   2. A real random token is generated at first init and persisted to
//      <dataDir>/owner.token (mode 0o600). The dashboard reads it from
//      a loopback-only endpoint and includes it as Bearer <token>.
//   3. authMiddleware does an EXACT match against owner.token. The
//      "device" sentinel is rejected.
//
// Combined: only the local owner of this machine can talk to the gateway.
// Stolen Mac without disk encryption → attacker can read the token file
// AND the SQLite DB → defense-in-depth requires FileVault/BitLocker
// (documented in README) plus the upcoming column-level encryption pass.
function getOwnerToken() {
    try {
        const tokenFile = join(getDataDir(), 'owner.token');
        if (!existsSync(tokenFile))
            return null;
        const tok = readFileSync(tokenFile, 'utf-8').trim();
        return tok.length >= 32 ? tok : null;
    }
    catch {
        return null;
    }
}
function deviceModeUser(token) {
    try {
        const ownerFile = join(getDataDir(), 'owner.id');
        if (!existsSync(ownerFile))
            return null;
        const id = readFileSync(ownerFile, 'utf-8').trim();
        if (id.length !== 36)
            return null;
        // Token check: the bearer MUST exactly match the persisted owner token.
        // We allow the legacy "device" sentinel only if owner.token doesn't
        // exist yet — gives existing 1.0.x installs a one-time grace period
        // until they next run init or the gateway boot regenerates the token.
        const expected = getOwnerToken();
        if (expected) {
            // (Audit L2) True constant-time compare — no early length-exit, the
            // loop runs over max(len) bytes regardless. Uses Node's
            // crypto.timingSafeEqual via Buffer.compare for cleanliness.
            if (!token)
                return null;
            const a = Buffer.from(token);
            const b = Buffer.from(expected);
            // Equalize lengths by padding the shorter buffer with zero bytes so
            // timingSafeEqual doesn't reject before comparing bytes.
            const len = Math.max(a.length, b.length);
            const padded = (buf) => {
                if (buf.length === len)
                    return buf;
                const out = Buffer.alloc(len);
                buf.copy(out);
                return out;
            };
            const ok = a.length === b.length && timingSafeEqual(padded(a), padded(b));
            if (!ok)
                return null;
        }
        else {
            // Legacy fallback: only the literal sentinel passes (still safer than
            // "any non-empty string"). On next gateway boot we generate the real
            // token and this path stops working.
            if (token !== 'device')
                return null;
            log.warn('Legacy "device" sentinel accepted — owner.token not yet generated');
        }
        const now = new Date().toISOString();
        return {
            id,
            app_metadata: {},
            user_metadata: { device_owner: true },
            aud: 'authenticated',
            created_at: now,
        };
    }
    catch (err) {
        log.warn({ error: err.message }, 'Failed to read device owner.id');
        return null;
    }
}
// ─── Decorators ──────────────────────────────────────────────────────
/**
 * تسجيل decorators على Fastify — يجب استدعاؤها مرة واحدة عند البدء
 */
export function registerAuthDecorators(app) {
    app.decorateRequest('user', null);
    app.decorateRequest('tenant', null);
}
// ─── Helpers ─────────────────────────────────────────────────────────
/**
 * استخراج التوكن من هيدر Authorization
 */
function extractBearerToken(request) {
    const authHeader = request.headers.authorization;
    if (!authHeader || !authHeader.startsWith('Bearer ')) {
        return null;
    }
    return authHeader.slice(7).trim();
}
/**
 * التحقق من التوكن واسترجاع بيانات المستخدم
 */
async function verifyToken(token) {
    const { data, error } = await supabase.auth.getUser(token);
    if (error || !data.user) {
        log.debug({ error: error?.message }, 'Token verification failed');
        return null;
    }
    return data.user;
}
// ─── Middleware ───────────────────────────────────────────────────────
/**
 * authMiddleware — التحقق الإلزامي من الهوية
 * يرفض الطلب بـ 401 إذا لم يكن هناك توكن صالح
 *
 * In device mode, the dashboard sends `Authorization: Bearer device` (or any
 * non-empty token) and we resolve the owner from the local owner.id file
 * instead of validating against Supabase Auth (which isn't running locally).
 */
export async function authMiddleware(request, reply) {
    const token = extractBearerToken(request);
    if (!token) {
        reply.status(401).send({
            success: false,
            error: 'Authentication required — يرجى تسجيل الدخول',
        });
        return;
    }
    if (config.MUASSIS_MODE === 'device') {
        const user = deviceModeUser(token);
        if (!user) {
            reply.status(401).send({
                success: false,
                error: 'Invalid token — احصل على التوكن من /api/auth/local-token (loopback فقط)',
            });
            return;
        }
        request.user = user;
        return;
    }
    const user = await verifyToken(token);
    if (!user) {
        reply.status(401).send({
            success: false,
            error: 'Invalid or expired token — التوكن غير صالح أو منتهي',
        });
        return;
    }
    request.user = user;
}
/**
 * optionalAuth — التحقق الاختياري من الهوية
 * لا يرفض الطلب إذا لم يكن هناك توكن، لكن يرفق بيانات المستخدم إذا وُجدت
 */
export async function optionalAuth(request, _reply) {
    const token = extractBearerToken(request);
    if (!token) {
        request.user = null;
        return;
    }
    const user = await verifyToken(token);
    request.user = user;
}
/**
 * Helper for any code path that needs the current user without going through
 * Fastify (e.g. background jobs, CLI). In device mode this skips the JWT path.
 *
 * Note: passes the in-process owner token because background jobs run as the
 * owner. External request paths still need the bearer to come from the wire.
 */
export function getDeviceOwnerOrNull() {
    return deviceModeUser(getOwnerToken() ?? 'device');
}
/**
 * tenantMiddleware — التحقق من ملكية المستأجر
 * يتطلب authMiddleware قبله — يتحقق أن المستخدم يملك المستأجر المطلوب
 */
export async function tenantMiddleware(request, reply) {
    const user = request.user;
    if (!user) {
        reply.status(401).send({
            success: false,
            error: 'Authentication required',
        });
        return;
    }
    // استخراج tenantId من المسار
    const params = request.params;
    const tenantId = params.tenantId || params.id;
    if (!tenantId) {
        reply.status(400).send({
            success: false,
            error: 'Tenant ID is required',
        });
        return;
    }
    const tenant = await getTenantById(tenantId);
    if (!tenant) {
        reply.status(404).send({
            success: false,
            error: 'Tenant not found — المستأجر غير موجود',
        });
        return;
    }
    // التحقق من الملكية عبر owner_id — يطابق auth.users.id من Supabase Auth.
    // Pre-007 schema had no owner_id column, so the check used to silently no-op.
    // Now: owner_id must exist AND match the authenticated user, else 403.
    if (!tenant.owner_id || tenant.owner_id !== user.id) {
        log.warn({ userId: user.id, tenantId, ownerId: tenant.owner_id ?? null }, 'Unauthorized tenant access attempt');
        reply.status(403).send({
            success: false,
            error: 'Access denied — ليس لديك صلاحية للوصول لهذا الحساب',
        });
        return;
    }
    request.tenant = tenant;
}
//# sourceMappingURL=middleware.js.map