FRE-4533: Merge apps/{api,web,mobile} and shared-db into ShieldAI repo
Merge FrenoCorp apps into ShieldAI packages/: - packages/api: merged routes (notifications), middleware (auth, rate-limit, error, logging), config, services (darkwatch, spamshield, voiceprint), tests - packages/web: new SolidJS web app stub - packages/mobile: new SolidJS mobile app stub - packages/shared-db: new Prisma DB package (separate from existing packages/db) - pnpm-workspace.yaml: restored (apps/* removed, already covered by packages/*) Next: reconcile packages/shared-db with packages/db, and fix server.ts correlationRoutes import
This commit is contained in:
155
packages/api/src/services/darkwatch/scheduler.service.ts
Normal file
155
packages/api/src/services/darkwatch/scheduler.service.ts
Normal file
@@ -0,0 +1,155 @@
|
||||
import { prisma, SubscriptionTier, SubscriptionStatus } from '@shieldsai/shared-db';
|
||||
import { tierConfig } from '@shieldsai/shared-billing';
|
||||
import { darkwatchScanQueue } from '@shieldsai/jobs';
|
||||
import { randomUUID } from 'crypto';
|
||||
|
||||
const CRON_EXPRESSIONS = {
|
||||
daily: '0 0 * * *',
|
||||
hourly: '0 * * * *',
|
||||
realtime: null,
|
||||
};
|
||||
|
||||
export class SchedulerService {
|
||||
async scheduleSubscriptionScans() {
|
||||
const activeSubscriptions = await prisma.subscription.findMany({
|
||||
where: {
|
||||
tier: { in: [SubscriptionTier.basic, SubscriptionTier.plus, SubscriptionTier.premium] },
|
||||
status: SubscriptionStatus.active,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
tier: true,
|
||||
userId: true,
|
||||
},
|
||||
});
|
||||
|
||||
const jobsEnqueued = [];
|
||||
|
||||
for (const subscription of activeSubscriptions) {
|
||||
const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency;
|
||||
const cron = CRON_EXPRESSIONS[frequency];
|
||||
|
||||
if (!cron) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const jobKey = `scheduled-scan:${subscription.id}`;
|
||||
|
||||
try {
|
||||
await darkwatchScanQueue.add(
|
||||
'scheduled-scan',
|
||||
{
|
||||
subscriptionId: subscription.id,
|
||||
tier: subscription.tier,
|
||||
scanType: 'scheduled',
|
||||
},
|
||||
{
|
||||
jobId: jobKey,
|
||||
repeat: {
|
||||
every: frequency === 'daily'
|
||||
? 24 * 60 * 60 * 1000
|
||||
: 60 * 60 * 1000,
|
||||
},
|
||||
priority: subscription.tier === SubscriptionTier.premium ? 1 : 3,
|
||||
}
|
||||
);
|
||||
|
||||
jobsEnqueued.push({
|
||||
subscriptionId: subscription.id,
|
||||
tier: subscription.tier,
|
||||
frequency,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as Error).message?.includes('Duplicate')) {
|
||||
continue;
|
||||
}
|
||||
console.error(
|
||||
`[SchedulerService] Failed to schedule scan for ${subscription.id}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return jobsEnqueued;
|
||||
}
|
||||
|
||||
async enqueueOnDemandScan(subscriptionId: string) {
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: { id: subscriptionId },
|
||||
select: { id: true, tier: true },
|
||||
});
|
||||
|
||||
if (!subscription) {
|
||||
throw new Error(`Subscription ${subscriptionId} not found`);
|
||||
}
|
||||
|
||||
return darkwatchScanQueue.add(
|
||||
'on-demand-scan',
|
||||
{
|
||||
subscriptionId,
|
||||
tier: subscription.tier,
|
||||
scanType: 'on-demand',
|
||||
},
|
||||
{
|
||||
priority: 1,
|
||||
jobId: `on-demand-scan:${subscriptionId}:${randomUUID()}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async enqueueRealtimeTrigger(subscriptionId: string, sourceData: Record<string, unknown>) {
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: { id: subscriptionId },
|
||||
select: { id: true, tier: true },
|
||||
});
|
||||
|
||||
if (!subscription || subscription.tier !== SubscriptionTier.premium) {
|
||||
throw new Error('Realtime triggers require Premium tier');
|
||||
}
|
||||
|
||||
return darkwatchScanQueue.add(
|
||||
'realtime-trigger',
|
||||
{
|
||||
subscriptionId,
|
||||
tier: subscription.tier,
|
||||
scanType: 'realtime',
|
||||
sourceData,
|
||||
},
|
||||
{
|
||||
priority: 0,
|
||||
jobId: `realtime-trigger:${subscriptionId}:${randomUUID()}`,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
async rescheduleAll() {
|
||||
const repeatableJobs = await darkwatchScanQueue.getRepeatableJobs();
|
||||
|
||||
for (const job of repeatableJobs) {
|
||||
await darkwatchScanQueue.removeRepeatableByKey(job.key);
|
||||
}
|
||||
|
||||
return this.scheduleSubscriptionScans();
|
||||
}
|
||||
|
||||
async getScanSchedule(subscriptionId: string) {
|
||||
const subscription = await prisma.subscription.findUnique({
|
||||
where: { id: subscriptionId },
|
||||
select: { tier: true },
|
||||
});
|
||||
|
||||
if (!subscription) return null;
|
||||
|
||||
const frequency = tierConfig[subscription.tier].features.darkWebScanFrequency;
|
||||
|
||||
return {
|
||||
subscriptionId,
|
||||
tier: subscription.tier,
|
||||
frequency,
|
||||
cron: CRON_EXPRESSIONS[frequency],
|
||||
nextRun: frequency === 'realtime' ? 'event-driven' : CRON_EXPRESSIONS[frequency],
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export const schedulerService = new SchedulerService();
|
||||
Reference in New Issue
Block a user