learning_ai_invt_trdg/backend/src/services/platformAuthService.ts

115 lines
4.0 KiB
TypeScript

import { createJwtUtils } from '@bytelyst/auth';
import { config } from '../config/index.js';
import logger from '../utils/logger.js';
import { supabaseService } from './SupabaseService.js';
export interface VerifiedTradingAuth {
userId: string | null;
role?: string;
email?: string;
displayName?: string;
plan?: string;
productId?: string;
source: 'platform' | 'supabase' | null;
error?: string;
}
let cachedJwtUtils: ReturnType<typeof createJwtUtils> | null | undefined;
function normalizeRole(role: unknown): string | undefined {
const value = String(role || '').trim().toLowerCase();
return value || undefined;
}
function getPlatformJwtUtils(): ReturnType<typeof createJwtUtils> | null {
if (cachedJwtUtils !== undefined) {
return cachedJwtUtils;
}
if (!config.PLATFORM_AUTH_ENABLED) {
cachedJwtUtils = null;
return cachedJwtUtils;
}
const hasHs256 = Boolean(config.JWT_SECRET);
const hasRs256 = Boolean(config.PLATFORM_JWT_PUBLIC_KEY || config.PLATFORM_JWT_JWKS_URL);
if (!hasHs256 && !hasRs256) {
cachedJwtUtils = null;
return cachedJwtUtils;
}
cachedJwtUtils = createJwtUtils({
issuer: config.PLATFORM_JWT_ISSUER,
algorithm: hasRs256 ? 'RS256' : 'HS256',
rsaPublicKey: config.PLATFORM_JWT_PUBLIC_KEY || undefined,
jwksUrl: config.PLATFORM_JWT_JWKS_URL || undefined,
});
return cachedJwtUtils;
}
export async function verifyTradingAccessToken(token: string): Promise<VerifiedTradingAuth> {
const jwtUtils = getPlatformJwtUtils();
if (jwtUtils) {
try {
const payload = await jwtUtils.verifyToken(token);
if (!payload) {
throw new Error('Platform token verification returned null payload');
}
const productId = String(payload?.productId || '').trim();
if (!payload?.sub) {
return { userId: null, source: null, error: 'Platform token missing subject claim' };
}
if (payload?.type && payload.type !== 'access') {
return { userId: null, source: null, error: 'Platform token is not an access token' };
}
if (productId && productId !== config.PRODUCT_ID) {
return { userId: null, source: null, error: `Platform token product mismatch (${productId})` };
}
return {
userId: String(payload.sub),
role: normalizeRole(payload.role),
email: typeof payload.email === 'string' ? payload.email : undefined,
displayName: typeof payload.displayName === 'string'
? payload.displayName
: typeof payload.name === 'string'
? payload.name
: undefined,
plan: typeof payload.plan === 'string' ? payload.plan : undefined,
productId: productId || config.PRODUCT_ID,
source: 'platform',
};
} catch (error) {
logger.warn(`[Auth] Platform token verification failed, falling back to legacy verifier: ${error instanceof Error ? error.message : 'unknown error'}`);
}
}
const legacy = await supabaseService.verifyAccessToken(token);
if (!legacy.userId) {
return {
userId: null,
source: null,
error: legacy.error || 'invalid token',
};
}
return {
userId: legacy.userId,
source: 'supabase',
};
}
/**
* Authoritative admin check for trading API scoping: platform JWT role first, then legacy user-store admin flag.
* Call sites should use this instead of SupabaseService.isAdmin so platform sessions stay consistent.
*/
export async function isTradingAdmin(userId: string, tokenRole?: string | null): Promise<boolean> {
const normalizedRole = normalizeRole(tokenRole);
if (normalizedRole === 'admin' || normalizedRole === 'super_admin') {
return true;
}
return supabaseService.isAdmin(userId);
}