Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz Source: /opt/alga-psa on psa.joliet.tech
191 lines
5.9 KiB
TypeScript
191 lines
5.9 KiB
TypeScript
import { NextRequest } from 'next/server';
|
|
import { getSession } from '@alga-psa/auth';
|
|
import { getAdminConnection } from '@alga-psa/db/admin';
|
|
|
|
export interface ExtProxyUserInfo {
|
|
user_id: string;
|
|
user_email: string;
|
|
user_name: string;
|
|
user_type: string;
|
|
client_name: string;
|
|
/** For client portal users, the client_id they are associated with */
|
|
client_id?: string;
|
|
/** Optional map of additional user attributes. */
|
|
additional_fields?: Record<string, string>;
|
|
}
|
|
|
|
function toNonEmptyString(value: unknown): string | undefined {
|
|
if (typeof value !== 'string') return undefined;
|
|
const trimmed = value.trim();
|
|
return trimmed.length > 0 ? trimmed : undefined;
|
|
}
|
|
|
|
function addScalarField(
|
|
target: Record<string, string>,
|
|
source: Record<string, unknown>,
|
|
key: string,
|
|
): void {
|
|
const value = source[key];
|
|
if (value === undefined || value === null) return;
|
|
if (typeof value === 'string') {
|
|
if (value.length > 0) target[key] = value;
|
|
return;
|
|
}
|
|
if (typeof value === 'number' || typeof value === 'boolean') {
|
|
target[key] = String(value);
|
|
}
|
|
}
|
|
|
|
function extractAdditionalFields(user: Record<string, unknown>): Record<string, string> {
|
|
const fields: Record<string, string> = {};
|
|
addScalarField(fields, user, 'contact_id');
|
|
addScalarField(fields, user, 'contactId');
|
|
addScalarField(fields, user, 'username');
|
|
addScalarField(fields, user, 'locale');
|
|
addScalarField(fields, user, 'timezone');
|
|
return fields;
|
|
}
|
|
|
|
/**
|
|
* Look up tenant's client_name from the database.
|
|
*/
|
|
async function getTenantClientName(tenantId: string): Promise<string> {
|
|
try {
|
|
const knex = await getAdminConnection();
|
|
const row = await knex('tenants')
|
|
.select('client_name')
|
|
.where('tenant', tenantId)
|
|
.first();
|
|
return row?.client_name || '';
|
|
} catch (error) {
|
|
console.error('[auth] Failed to look up tenant client_name:', error);
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Look up user's client_id from their contact association.
|
|
* Returns undefined if user doesn't have a contact or contact doesn't have a client.
|
|
*/
|
|
async function getUserClientId(userId: string, tenantId: string): Promise<string | undefined> {
|
|
try {
|
|
const knex = await getAdminConnection();
|
|
// First get the user's contact_id, then look up the client_id from contacts
|
|
const user = await knex('users')
|
|
.select('contact_id')
|
|
.where('user_id', userId)
|
|
.where('tenant', tenantId)
|
|
.first();
|
|
|
|
if (!user?.contact_id) {
|
|
return undefined;
|
|
}
|
|
|
|
const contact = await knex('contacts')
|
|
.select('client_id')
|
|
.where('contact_name_id', user.contact_id)
|
|
.where('tenant', tenantId)
|
|
.first();
|
|
|
|
return contact?.client_id || undefined;
|
|
} catch (error) {
|
|
console.error('[auth] Failed to look up user client_id:', error);
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get full user info from session for passing to runner.
|
|
* Returns null if no valid session exists.
|
|
*/
|
|
export async function getUserInfoFromAuth(req: NextRequest): Promise<ExtProxyUserInfo | null> {
|
|
// Check for internal header first (not typically used for user info)
|
|
const headerTenant = req.headers.get('x-alga-tenant');
|
|
if (headerTenant) {
|
|
// When using header-based auth, we don't have user info
|
|
console.log('[ext-proxy auth] Skipping user info - x-alga-tenant header present');
|
|
return null;
|
|
}
|
|
|
|
const session = await getSession();
|
|
const user = session?.user as any;
|
|
|
|
console.log('[ext-proxy auth] Session check', {
|
|
hasSession: !!session,
|
|
hasUser: !!user,
|
|
userId: user?.user_id || user?.id,
|
|
userEmail: user?.email,
|
|
});
|
|
|
|
if (!user) {
|
|
return null;
|
|
}
|
|
|
|
// Look up tenant's client_name from database
|
|
const tenantId = user.tenant || '';
|
|
const clientName = tenantId ? await getTenantClientName(tenantId) : '';
|
|
|
|
// Use the client ID carried in the auth token/session when available.
|
|
// This is the most reliable source for client portal sessions.
|
|
const sessionClientId =
|
|
toNonEmptyString((user as Record<string, unknown>).client_id) ??
|
|
toNonEmptyString((user as Record<string, unknown>).clientId);
|
|
|
|
// Fall back to DB lookup only when needed.
|
|
const userId = user.user_id || user.id || '';
|
|
const userType = user.user_type || user.userType || 'internal';
|
|
const clientId =
|
|
sessionClientId ||
|
|
((userType === 'client' && userId && tenantId)
|
|
? await getUserClientId(userId, tenantId)
|
|
: undefined);
|
|
|
|
const userInfo: ExtProxyUserInfo = {
|
|
user_id: userId,
|
|
user_email: user.email || '',
|
|
user_name: user.name || user.username || '',
|
|
user_type: userType,
|
|
client_name: clientName,
|
|
client_id: clientId,
|
|
additional_fields: extractAdditionalFields(user as Record<string, unknown>),
|
|
};
|
|
|
|
console.log('[ext-proxy auth] Returning user info', {
|
|
userId: userInfo.user_id,
|
|
userEmail: userInfo.user_email,
|
|
userName: userInfo.user_name,
|
|
userType: userInfo.user_type,
|
|
clientId: userInfo.client_id,
|
|
});
|
|
|
|
return userInfo;
|
|
}
|
|
|
|
export async function getTenantFromAuth(req: NextRequest): Promise<string> {
|
|
// Minimal scaffolding:
|
|
// - Prefer internal header `x-alga-tenant` (e.g., set by edge/auth middleware)
|
|
// - Fallback to DEV_TENANT_ID for local development
|
|
// - Otherwise, reject (to avoid running as a fake tenant)
|
|
const h = req.headers.get('x-alga-tenant');
|
|
if (h && h.trim()) return h.trim();
|
|
|
|
// Accept legacy header used by admin/publishing clients.
|
|
const legacy = req.headers.get('x-tenant-id');
|
|
if (legacy && legacy.trim()) return legacy.trim();
|
|
|
|
const session = await getSession();
|
|
const sessionTenant = (session?.user as any)?.tenant;
|
|
if (sessionTenant && String(sessionTenant).trim()) {
|
|
return String(sessionTenant).trim();
|
|
}
|
|
|
|
const dev = process.env.DEV_TENANT_ID;
|
|
if (dev && dev.trim()) return dev.trim();
|
|
throw new Error('unauthenticated');
|
|
}
|
|
|
|
export async function assertAccess(_tenantId: string, _extensionId: string, _method: string, _path: string): Promise<void> {
|
|
// TODO: implement RBAC and per-tenant endpoint checks
|
|
return;
|
|
}
|