Phase 1: Forward Assist initial build
Multi-tenant AI help desk SaaS for the firearms industry. Full monorepo: API (Express/Prisma), Worker (BullMQ), Frontend (React/Vite/Tailwind). PostgreSQL 16 + pgvector, Redis 7, JWT auth, RLS tenant isolation. Dark Armory theme with tactical branding throughout. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
import { prisma } from './prisma';
|
||||
import { Request } from 'express';
|
||||
|
||||
interface AuditEntry {
|
||||
tenantId: string;
|
||||
userId?: string;
|
||||
action: string;
|
||||
entity: string;
|
||||
entityId?: string;
|
||||
details?: Record<string, unknown>;
|
||||
req?: Request;
|
||||
}
|
||||
|
||||
export async function logAudit(entry: AuditEntry): Promise<void> {
|
||||
try {
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
tenantId: entry.tenantId,
|
||||
userId: entry.userId,
|
||||
action: entry.action,
|
||||
entity: entry.entity,
|
||||
entityId: entry.entityId,
|
||||
details: entry.details || {},
|
||||
ipAddress: entry.req?.ip || entry.req?.socket.remoteAddress,
|
||||
userAgent: entry.req?.headers['user-agent'],
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
// Audit logging should never crash the app
|
||||
console.error('[Audit] Failed to log:', error);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import jwt from 'jsonwebtoken';
|
||||
|
||||
const ACCESS_SECRET = process.env.JWT_ACCESS_SECRET || 'dev-access-secret-minimum-32-characters';
|
||||
const REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'dev-refresh-secret-minimum-32-characters';
|
||||
const ACCESS_EXPIRY = process.env.JWT_ACCESS_EXPIRY || '15m';
|
||||
const REFRESH_EXPIRY = process.env.JWT_REFRESH_EXPIRY || '7d';
|
||||
|
||||
export interface TokenPayload {
|
||||
userId: string;
|
||||
tenantId: string;
|
||||
email: string;
|
||||
role: string;
|
||||
}
|
||||
|
||||
export function signAccessToken(payload: TokenPayload): string {
|
||||
return jwt.sign(payload, ACCESS_SECRET, { expiresIn: ACCESS_EXPIRY });
|
||||
}
|
||||
|
||||
export function signRefreshToken(payload: TokenPayload): string {
|
||||
return jwt.sign(payload, REFRESH_SECRET, { expiresIn: REFRESH_EXPIRY });
|
||||
}
|
||||
|
||||
export function verifyAccessToken(token: string): TokenPayload {
|
||||
return jwt.verify(token, ACCESS_SECRET) as TokenPayload;
|
||||
}
|
||||
|
||||
export function verifyRefreshToken(token: string): TokenPayload {
|
||||
return jwt.verify(token, REFRESH_SECRET) as TokenPayload;
|
||||
}
|
||||
|
||||
export function generateTokenPair(payload: TokenPayload) {
|
||||
return {
|
||||
accessToken: signAccessToken(payload),
|
||||
refreshToken: signRefreshToken(payload),
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { PrismaClient } from '@prisma/client';
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? new PrismaClient({
|
||||
log: process.env.NODE_ENV === 'development' ? ['warn', 'error'] : ['error'],
|
||||
});
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
Reference in New Issue
Block a user