CognxSafeTrack commited on
Commit ·
9b8717a
1
Parent(s): 1fa2a07
chore: deploy manual fixes for TS strict mode
Browse files
apps/api/src/index.ts
CHANGED
|
@@ -64,7 +64,7 @@ const registerRoutes = async () => {
|
|
| 64 |
if (request.method === 'OPTIONS') return;
|
| 65 |
|
| 66 |
const apiKey = process.env.ADMIN_API_KEY;
|
| 67 |
-
|
| 68 |
// Allow Admin API Key
|
| 69 |
if (apiKey && request.headers['x-api-key'] === apiKey) return;
|
| 70 |
|
|
@@ -131,18 +131,18 @@ server.get('/health', async (_req, reply) => {
|
|
| 131 |
const start = async () => {
|
| 132 |
try {
|
| 133 |
await registerRoutes();
|
| 134 |
-
|
| 135 |
await setupRateLimit(server);
|
| 136 |
|
| 137 |
const port = parseInt(process.env.PORT || '8080');
|
| 138 |
logger.info(`[STARTUP] Attempting to listen on port ${port}...`);
|
| 139 |
-
|
| 140 |
await server.listen({ port, host: '0.0.0.0' });
|
| 141 |
logger.info(`🚀 Server listening on http://0.0.0.0:${port}`);
|
| 142 |
|
| 143 |
// Background tasks (after listen, safe to start)
|
| 144 |
startCleanupCron();
|
| 145 |
-
|
| 146 |
} catch (err: any) {
|
| 147 |
logger.error({ err }, '[STARTUP] ❌ FATAL ERROR DURING STARTUP');
|
| 148 |
process.exit(1);
|
|
@@ -152,7 +152,7 @@ const start = async () => {
|
|
| 152 |
// ── Graceful Shutdown ─────────────────────────────────────────────────────────
|
| 153 |
const handleShutdown = async (signal: string) => {
|
| 154 |
logger.info(`[SHUTDOWN] 🛑 Received ${signal}. Starting graceful shutdown...`);
|
| 155 |
-
|
| 156 |
// 1. Stop accepting new requests
|
| 157 |
await server.close();
|
| 158 |
logger.info('[SHUTDOWN] HTTP server closed.');
|
|
|
|
| 64 |
if (request.method === 'OPTIONS') return;
|
| 65 |
|
| 66 |
const apiKey = process.env.ADMIN_API_KEY;
|
| 67 |
+
|
| 68 |
// Allow Admin API Key
|
| 69 |
if (apiKey && request.headers['x-api-key'] === apiKey) return;
|
| 70 |
|
|
|
|
| 131 |
const start = async () => {
|
| 132 |
try {
|
| 133 |
await registerRoutes();
|
| 134 |
+
|
| 135 |
await setupRateLimit(server);
|
| 136 |
|
| 137 |
const port = parseInt(process.env.PORT || '8080');
|
| 138 |
logger.info(`[STARTUP] Attempting to listen on port ${port}...`);
|
| 139 |
+
|
| 140 |
await server.listen({ port, host: '0.0.0.0' });
|
| 141 |
logger.info(`🚀 Server listening on http://0.0.0.0:${port}`);
|
| 142 |
|
| 143 |
// Background tasks (after listen, safe to start)
|
| 144 |
startCleanupCron();
|
| 145 |
+
|
| 146 |
} catch (err: any) {
|
| 147 |
logger.error({ err }, '[STARTUP] ❌ FATAL ERROR DURING STARTUP');
|
| 148 |
process.exit(1);
|
|
|
|
| 152 |
// ── Graceful Shutdown ─────────────────────────────────────────────────────────
|
| 153 |
const handleShutdown = async (signal: string) => {
|
| 154 |
logger.info(`[SHUTDOWN] 🛑 Received ${signal}. Starting graceful shutdown...`);
|
| 155 |
+
|
| 156 |
// 1. Stop accepting new requests
|
| 157 |
await server.close();
|
| 158 |
logger.info('[SHUTDOWN] HTTP server closed.');
|
apps/api/src/routes/admin.ts
CHANGED
|
@@ -58,7 +58,7 @@ export async function adminRoutes(fastify: FastifyInstance) {
|
|
| 58 |
fastify.get('/users', async (req, reply) => {
|
| 59 |
const parsed = PaginationSchema.safeParse(req.query);
|
| 60 |
if (!parsed.success) return reply.code(400).send({ error: 'Invalid query parameters', details: parsed.error.flatten() });
|
| 61 |
-
|
| 62 |
const { page, limit } = parsed.data;
|
| 63 |
|
| 64 |
const [users, total] = await Promise.all([
|
|
|
|
| 58 |
fastify.get('/users', async (req, reply) => {
|
| 59 |
const parsed = PaginationSchema.safeParse(req.query);
|
| 60 |
if (!parsed.success) return reply.code(400).send({ error: 'Invalid query parameters', details: parsed.error.flatten() });
|
| 61 |
+
|
| 62 |
const { page, limit } = parsed.data;
|
| 63 |
|
| 64 |
const [users, total] = await Promise.all([
|
apps/api/src/routes/analytics.ts
CHANGED
|
@@ -10,7 +10,7 @@ export async function analyticsRoutes(fastify: FastifyInstance) {
|
|
| 10 |
*/
|
| 11 |
fastify.get('/usage', async (req, reply) => {
|
| 12 |
const organizationId = (req as any).organizationId;
|
| 13 |
-
|
| 14 |
if (!organizationId) {
|
| 15 |
return reply.code(400).send({ error: 'Organization ID is required' });
|
| 16 |
}
|
|
@@ -27,16 +27,16 @@ export async function analyticsRoutes(fastify: FastifyInstance) {
|
|
| 27 |
prisma.message.count({ where: { organizationId, direction: 'INBOUND' } }),
|
| 28 |
prisma.message.count({ where: { organizationId, direction: 'OUTBOUND' } }),
|
| 29 |
prisma.user.count({ where: { organizationId } }),
|
| 30 |
-
prisma.user.count({
|
| 31 |
-
where: {
|
| 32 |
-
organizationId,
|
| 33 |
-
lastActivityAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
|
| 34 |
-
}
|
| 35 |
})
|
| 36 |
]);
|
| 37 |
|
| 38 |
// Estimate costs (Simplified: 1000 tokens avg per message interaction)
|
| 39 |
-
const estimatedTokens = totalMessages * 1000;
|
| 40 |
|
| 41 |
return {
|
| 42 |
messages: {
|
|
@@ -80,8 +80,8 @@ export async function analyticsRoutes(fastify: FastifyInstance) {
|
|
| 80 |
const completed = enrollments.filter(e => e.status === 'COMPLETED').length;
|
| 81 |
const active = enrollments.filter(e => e.status === 'ACTIVE').length;
|
| 82 |
|
| 83 |
-
const averageProgress = total > 0
|
| 84 |
-
? enrollments.reduce((acc, curr) => acc + curr.currentDay, 0) / total
|
| 85 |
: 0;
|
| 86 |
|
| 87 |
const scores = await prisma.userProgress.aggregate({
|
|
@@ -139,10 +139,10 @@ export async function analyticsRoutes(fastify: FastifyInstance) {
|
|
| 139 |
});
|
| 140 |
|
| 141 |
const total = counts.SENT + counts.DELIVERED + counts.READ + counts.FAILED;
|
| 142 |
-
|
| 143 |
// Funnel logic: DELIVERED usually implies it was SENT, etc.
|
| 144 |
// But here we count specific statuses.
|
| 145 |
-
|
| 146 |
return {
|
| 147 |
summary: {
|
| 148 |
total,
|
|
|
|
| 10 |
*/
|
| 11 |
fastify.get('/usage', async (req, reply) => {
|
| 12 |
const organizationId = (req as any).organizationId;
|
| 13 |
+
|
| 14 |
if (!organizationId) {
|
| 15 |
return reply.code(400).send({ error: 'Organization ID is required' });
|
| 16 |
}
|
|
|
|
| 27 |
prisma.message.count({ where: { organizationId, direction: 'INBOUND' } }),
|
| 28 |
prisma.message.count({ where: { organizationId, direction: 'OUTBOUND' } }),
|
| 29 |
prisma.user.count({ where: { organizationId } }),
|
| 30 |
+
prisma.user.count({
|
| 31 |
+
where: {
|
| 32 |
+
organizationId,
|
| 33 |
+
lastActivityAt: { gte: new Date(Date.now() - 24 * 60 * 60 * 1000) }
|
| 34 |
+
}
|
| 35 |
})
|
| 36 |
]);
|
| 37 |
|
| 38 |
// Estimate costs (Simplified: 1000 tokens avg per message interaction)
|
| 39 |
+
const estimatedTokens = totalMessages * 1000;
|
| 40 |
|
| 41 |
return {
|
| 42 |
messages: {
|
|
|
|
| 80 |
const completed = enrollments.filter(e => e.status === 'COMPLETED').length;
|
| 81 |
const active = enrollments.filter(e => e.status === 'ACTIVE').length;
|
| 82 |
|
| 83 |
+
const averageProgress = total > 0
|
| 84 |
+
? enrollments.reduce((acc, curr) => acc + curr.currentDay, 0) / total
|
| 85 |
: 0;
|
| 86 |
|
| 87 |
const scores = await prisma.userProgress.aggregate({
|
|
|
|
| 139 |
});
|
| 140 |
|
| 141 |
const total = counts.SENT + counts.DELIVERED + counts.READ + counts.FAILED;
|
| 142 |
+
|
| 143 |
// Funnel logic: DELIVERED usually implies it was SENT, etc.
|
| 144 |
// But here we count specific statuses.
|
| 145 |
+
|
| 146 |
return {
|
| 147 |
summary: {
|
| 148 |
total,
|
apps/api/src/routes/organizations.ts
CHANGED
|
@@ -41,7 +41,7 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 41 |
fastify.post('/', async (req, reply) => {
|
| 42 |
const body = OrganizationCreationSchema.safeParse(req.body);
|
| 43 |
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
|
| 44 |
-
|
| 45 |
const { adminEmail, adminName, slug, mode, ...orgData } = body.data;
|
| 46 |
|
| 47 |
// Check if slug already exists
|
|
@@ -49,7 +49,7 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 49 |
if (existing) return reply.code(400).send({ error: 'Slug already taken' });
|
| 50 |
|
| 51 |
const data = encryptSecrets(orgData);
|
| 52 |
-
|
| 53 |
// Use a transaction to ensure Org, Admin and Email Queueing are linked
|
| 54 |
const result = await prisma.$transaction(async (tx) => {
|
| 55 |
const org = await tx.organization.create({
|
|
@@ -73,7 +73,7 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 73 |
// Send Welcome Email (async via BullMQ) inside transaction to link with creation success
|
| 74 |
const loginUrl = `https://${slug}.xamle.studio/login`;
|
| 75 |
const resetUrl = `https://${slug}.xamle.studio/reset-password`;
|
| 76 |
-
|
| 77 |
await scheduleEmail({
|
| 78 |
to: adminEmail,
|
| 79 |
subject: `Bienvenue chez Xamlé Studio - ${org.name}`,
|
|
@@ -158,12 +158,12 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 158 |
await tx.whatsAppPhoneNumber.upsert({
|
| 159 |
where: { id: phoneNumberId },
|
| 160 |
update: {
|
| 161 |
-
|
| 162 |
organizationId: id
|
| 163 |
},
|
| 164 |
create: {
|
| 165 |
id: phoneNumberId,
|
| 166 |
-
|
| 167 |
organizationId: id
|
| 168 |
}
|
| 169 |
});
|
|
@@ -215,7 +215,7 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 215 |
fastify.post('/:id/index-kb', async (req, reply) => {
|
| 216 |
const { id } = req.params as { id: string };
|
| 217 |
const org = await prisma.organization.findUnique({ where: { id } });
|
| 218 |
-
|
| 219 |
if (!org || !org.knowledgeBaseUrl) {
|
| 220 |
return reply.code(400).send({ error: 'Organization or Knowledge Base URL not found' });
|
| 221 |
}
|
|
@@ -301,7 +301,7 @@ export async function organizationRoutes(fastify: FastifyInstance) {
|
|
| 301 |
// 10. CRM: Delete Single Contact
|
| 302 |
fastify.delete('/:id/contacts/:contactId', async (req, reply) => {
|
| 303 |
const { id: organizationId, contactId } = req.params as { id: string; contactId: string };
|
| 304 |
-
|
| 305 |
try {
|
| 306 |
await prisma.contact.delete({
|
| 307 |
where: { id: contactId, organizationId } // Security check
|
|
|
|
| 41 |
fastify.post('/', async (req, reply) => {
|
| 42 |
const body = OrganizationCreationSchema.safeParse(req.body);
|
| 43 |
if (!body.success) return reply.code(400).send({ error: body.error.flatten() });
|
| 44 |
+
|
| 45 |
const { adminEmail, adminName, slug, mode, ...orgData } = body.data;
|
| 46 |
|
| 47 |
// Check if slug already exists
|
|
|
|
| 49 |
if (existing) return reply.code(400).send({ error: 'Slug already taken' });
|
| 50 |
|
| 51 |
const data = encryptSecrets(orgData);
|
| 52 |
+
|
| 53 |
// Use a transaction to ensure Org, Admin and Email Queueing are linked
|
| 54 |
const result = await prisma.$transaction(async (tx) => {
|
| 55 |
const org = await tx.organization.create({
|
|
|
|
| 73 |
// Send Welcome Email (async via BullMQ) inside transaction to link with creation success
|
| 74 |
const loginUrl = `https://${slug}.xamle.studio/login`;
|
| 75 |
const resetUrl = `https://${slug}.xamle.studio/reset-password`;
|
| 76 |
+
|
| 77 |
await scheduleEmail({
|
| 78 |
to: adminEmail,
|
| 79 |
subject: `Bienvenue chez Xamlé Studio - ${org.name}`,
|
|
|
|
| 158 |
await tx.whatsAppPhoneNumber.upsert({
|
| 159 |
where: { id: phoneNumberId },
|
| 160 |
update: {
|
| 161 |
+
displayPhone: phoneNumber || '',
|
| 162 |
organizationId: id
|
| 163 |
},
|
| 164 |
create: {
|
| 165 |
id: phoneNumberId,
|
| 166 |
+
displayPhone: phoneNumber || '',
|
| 167 |
organizationId: id
|
| 168 |
}
|
| 169 |
});
|
|
|
|
| 215 |
fastify.post('/:id/index-kb', async (req, reply) => {
|
| 216 |
const { id } = req.params as { id: string };
|
| 217 |
const org = await prisma.organization.findUnique({ where: { id } });
|
| 218 |
+
|
| 219 |
if (!org || !org.knowledgeBaseUrl) {
|
| 220 |
return reply.code(400).send({ error: 'Organization or Knowledge Base URL not found' });
|
| 221 |
}
|
|
|
|
| 301 |
// 10. CRM: Delete Single Contact
|
| 302 |
fastify.delete('/:id/contacts/:contactId', async (req, reply) => {
|
| 303 |
const { id: organizationId, contactId } = req.params as { id: string; contactId: string };
|
| 304 |
+
|
| 305 |
try {
|
| 306 |
await prisma.contact.delete({
|
| 307 |
where: { id: contactId, organizationId } // Security check
|
apps/api/src/routes/payments.ts
CHANGED
|
@@ -65,24 +65,24 @@ export async function paymentRoutes(fastify: FastifyInstance) {
|
|
| 65 |
return reply.status(500).send({ error: 'Failed to create organization checkout session' });
|
| 66 |
}
|
| 67 |
});
|
| 68 |
-
|
| 69 |
// Create a Billing Portal Session
|
| 70 |
fastify.post('/customer-portal', async (request, reply) => {
|
| 71 |
const organizationId = (request as any).organizationId;
|
| 72 |
if (!organizationId) {
|
| 73 |
return reply.status(400).send({ error: 'Missing organizationId' });
|
| 74 |
}
|
| 75 |
-
|
| 76 |
try {
|
| 77 |
const org = await prisma.organization.findUnique({
|
| 78 |
where: { id: organizationId },
|
| 79 |
select: { stripeCustomerId: true }
|
| 80 |
});
|
| 81 |
-
|
| 82 |
if (!org?.stripeCustomerId) {
|
| 83 |
return reply.status(400).send({ error: 'No active subscription found for this organization' });
|
| 84 |
}
|
| 85 |
-
|
| 86 |
const portalUrl = await stripeService.createCustomerPortalSession(org.stripeCustomerId);
|
| 87 |
return { success: true, url: portalUrl };
|
| 88 |
} catch (error) {
|
|
@@ -125,7 +125,7 @@ export async function stripeWebhookRoute(fastify: FastifyInstance) {
|
|
| 125 |
}
|
| 126 |
|
| 127 |
// --- Handle Events ---
|
| 128 |
-
|
| 129 |
// 1. Single Payments (Student enrolling in premium track)
|
| 130 |
if (event.type === 'checkout.session.completed') {
|
| 131 |
const session = event.data.object as any;
|
|
@@ -172,7 +172,7 @@ export async function stripeWebhookRoute(fastify: FastifyInstance) {
|
|
| 172 |
// This was a subscription checkout for an organization
|
| 173 |
await prisma.organization.update({
|
| 174 |
where: { id: orgId },
|
| 175 |
-
data: {
|
| 176 |
stripeCustomerId: session.customer as string,
|
| 177 |
subscriptionStatus: 'ACTIVE'
|
| 178 |
}
|
|
|
|
| 65 |
return reply.status(500).send({ error: 'Failed to create organization checkout session' });
|
| 66 |
}
|
| 67 |
});
|
| 68 |
+
|
| 69 |
// Create a Billing Portal Session
|
| 70 |
fastify.post('/customer-portal', async (request, reply) => {
|
| 71 |
const organizationId = (request as any).organizationId;
|
| 72 |
if (!organizationId) {
|
| 73 |
return reply.status(400).send({ error: 'Missing organizationId' });
|
| 74 |
}
|
| 75 |
+
|
| 76 |
try {
|
| 77 |
const org = await prisma.organization.findUnique({
|
| 78 |
where: { id: organizationId },
|
| 79 |
select: { stripeCustomerId: true }
|
| 80 |
});
|
| 81 |
+
|
| 82 |
if (!org?.stripeCustomerId) {
|
| 83 |
return reply.status(400).send({ error: 'No active subscription found for this organization' });
|
| 84 |
}
|
| 85 |
+
|
| 86 |
const portalUrl = await stripeService.createCustomerPortalSession(org.stripeCustomerId);
|
| 87 |
return { success: true, url: portalUrl };
|
| 88 |
} catch (error) {
|
|
|
|
| 125 |
}
|
| 126 |
|
| 127 |
// --- Handle Events ---
|
| 128 |
+
|
| 129 |
// 1. Single Payments (Student enrolling in premium track)
|
| 130 |
if (event.type === 'checkout.session.completed') {
|
| 131 |
const session = event.data.object as any;
|
|
|
|
| 172 |
// This was a subscription checkout for an organization
|
| 173 |
await prisma.organization.update({
|
| 174 |
where: { id: orgId },
|
| 175 |
+
data: {
|
| 176 |
stripeCustomerId: session.customer as string,
|
| 177 |
subscriptionStatus: 'ACTIVE'
|
| 178 |
}
|
packages/shared-types/tsconfig.tsbuildinfo
DELETED
|
@@ -1 +0,0 @@
|
|
| 1 |
-
{"root":["./src/index.ts"],"version":"5.9.3"}
|
|
|
|
|
|