api: spam check settings, lookups and cache
SD1 (14 §9): the spam/lookups tables and calls columns, the IPQualityScore and FTC lookups with the cache, the spam settings with write-only keys, the provider Test routes and Settings → Spam check.
This commit is contained in:
1 parent
a3dfcb6b14
commit
6f87d6f663
22 files changed
+5907
-23
No files matched your search
@@ -27,3 +27,4 @@ export * from './blocked.ts';
|
||||
export * from './calls.ts';
|
||||
export * from './contacts.ts';
|
||||
export * from './integrations.ts';
|
||||
export * from './spam.ts';
|
||||
@@ -2,6 +2,12 @@
|
||||
// One zod schema (with its default) per settings key (spec 02 §3).
|
||||
import { z } from 'zod';
|
||||
import { DestinationSchema } from './destination.ts';
|
||||
import {
|
||||
SPAM_DEFAULTS,
|
||||
SpamSettingsSchema,
|
||||
SpamStateSchema,
|
||||
type SpamSettingsDto,
|
||||
} from './spam.ts';
|
||||
|
||||
const range = (from: number, to: number) =>
|
||||
z
|
||||
@@ -151,6 +157,10 @@ export const SettingsSchemas = {
|
||||
})
|
||||
.nullable()
|
||||
.default(null),
|
||||
/** Spam check (spec 14 §7): off until the admin turns it on. */
|
||||
spam: SpamSettingsSchema.default(SPAM_DEFAULTS),
|
||||
/** What the system keeps about the spam check's providers (14 §3, §2.2). */
|
||||
'spam.state': SpamStateSchema.default(SpamStateSchema.parse({})),
|
||||
/** Why the directory couldn't be used (12 §4.2, §4.5), for the dashboard; null = fine. */
|
||||
'system.directoryWarning': z.string().nullable().default(null),
|
||||
// Not in 02 §3's table but used by the generator (03 §3).
|
||||
@@ -317,12 +327,13 @@ export type SettingsDto = {
|
||||
[
|
||||
K in Exclude<
|
||||
SettingKey,
|
||||
'smtp' | 'sms.webhookToken' | 'contacts.contrack' | 'auth.oidc' | 'auth.ldap'
|
||||
'smtp' | 'sms.webhookToken' | 'contacts.contrack' | 'auth.oidc' | 'auth.ldap' | 'spam'
|
||||
>
|
||||
]: SettingValue<K>;
|
||||
} & {
|
||||
smtp: (Omit<SmtpInput, 'password'> & { hasPassword: boolean }) | null;
|
||||
'contacts.contrack': { url: string; publicUrl: string | null; hasCompanyToken: boolean } | null;
|
||||
spam: SpamSettingsDto;
|
||||
'auth.oidc':
|
||||
(Omit<OidcInput, 'clientSecret' | 'clearClientSecret'> & { hasClientSecret: boolean }) | null;
|
||||
'auth.ldap':
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
// Spam check (spec 14): what a check says about an outside caller, and its settings.
|
||||
import { z } from 'zod';
|
||||
|
||||
export type SpamRisk = 'low' | 'medium' | 'high';
|
||||
export type SpamProvider = 'ipqs' | 'ftc';
|
||||
|
||||
/** IPQualityScore's answer, only the fields 14 §2.1 keeps. */
|
||||
export type IpqsInfo = {
|
||||
fraudScore: number;
|
||||
spammer: boolean;
|
||||
recentAbuse: boolean;
|
||||
lineType: string | null;
|
||||
carrier: string | null;
|
||||
name: string | null;
|
||||
city: string | null;
|
||||
region: string | null;
|
||||
};
|
||||
|
||||
/** The FTC complaints in the window (14 §2.2). */
|
||||
export type FtcInfo = {
|
||||
complaints: number;
|
||||
robocalls: number;
|
||||
lastReportedAt: string | null;
|
||||
subject: string | null;
|
||||
};
|
||||
|
||||
export type SpamCheck =
|
||||
| { risk: SpamRisk; provider: 'ipqs'; info: IpqsInfo }
|
||||
| { risk: SpamRisk; provider: 'ftc'; info: FtcInfo };
|
||||
|
||||
/** The desk phone labels go through phoneSafeName (api), so only what it keeps. */
|
||||
const label = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1)
|
||||
.max(20)
|
||||
.regex(/^[\p{L}\p{M}\p{N} .'&@-]+$/u, "Letters, digits, spaces and . ' & @ - only")
|
||||
.transform((s) => s.replace(/ +/g, ' '));
|
||||
|
||||
export const SpamLabelsSchema = z.object({ high: label, medium: label });
|
||||
|
||||
export const SpamCacheSchema = z.object({
|
||||
flaggedDays: z.number().int().min(1).max(365),
|
||||
cleanDays: z.number().int().min(1).max(365),
|
||||
});
|
||||
|
||||
/** The stored `spam` setting (14 §7). Keys encrypted; a null provider isn't used. */
|
||||
export const SpamSettingsSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
ipqs: z.object({ keyEnc: z.string() }).nullable(),
|
||||
ftc: z
|
||||
.object({
|
||||
keyEnc: z.string(),
|
||||
syncHours: z.number().int().min(1).max(168),
|
||||
windowDays: z.number().int().min(7).max(365),
|
||||
})
|
||||
.nullable(),
|
||||
cache: SpamCacheSchema,
|
||||
deskPhoneLabels: SpamLabelsSchema,
|
||||
});
|
||||
export type SpamSettings = z.infer<typeof SpamSettingsSchema>;
|
||||
|
||||
export const SPAM_DEFAULTS: SpamSettings = {
|
||||
enabled: false,
|
||||
ipqs: null,
|
||||
ftc: null,
|
||||
cache: { flaggedDays: 14, cleanDays: 3 },
|
||||
deskPhoneLabels: { high: 'SPAM LIKELY', medium: 'Spam risk' },
|
||||
};
|
||||
|
||||
/** What the system keeps about the providers (`spam.state`, read-only to PATCH). */
|
||||
export const SpamStateSchema = z.object({
|
||||
ipqsPausedAt: z.string().nullable().default(null),
|
||||
warningDismissedAt: z.string().nullable().default(null),
|
||||
ftcSyncedTo: z.string().nullable().default(null),
|
||||
ftcCursor: z.number().int().nullable().default(null),
|
||||
ftcLastSyncAt: z.string().nullable().default(null),
|
||||
ftcLastError: z.string().nullable().default(null),
|
||||
});
|
||||
export type SpamState = z.infer<typeof SpamStateSchema>;
|
||||
|
||||
/** PATCH /api/settings `spam`: keys are write-only (empty keeps the stored one). */
|
||||
export const SpamInputSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
ipqs: z.object({ key: z.string().trim().max(200).optional() }).nullable(),
|
||||
ftc: z
|
||||
.object({
|
||||
key: z.string().trim().max(200).optional(),
|
||||
syncHours: z.number().int().min(1).max(168),
|
||||
windowDays: z.number().int().min(7).max(365),
|
||||
})
|
||||
.nullable(),
|
||||
cache: SpamCacheSchema,
|
||||
deskPhoneLabels: SpamLabelsSchema,
|
||||
});
|
||||
export type SpamInput = z.input<typeof SpamInputSchema>;
|
||||
|
||||
/** GET /api/settings `spam`: keys replaced by hasKey. */
|
||||
export type SpamSettingsDto = Omit<SpamSettings, 'ipqs' | 'ftc'> & {
|
||||
ipqs: { hasKey: boolean } | null;
|
||||
ftc: { hasKey: boolean; syncHours: number; windowDays: number } | null;
|
||||
};
|
||||
|
||||
/** POST /api/settings/spam/{ipqs,ftc}/test. */
|
||||
export type SpamTestResult = { ok: true; detail: string } | { ok: false; error: string };
|
||||
Reference in new issue
Block a user