115 lines
4.0 KiB
TypeScript
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);
|
|
}
|