feat(14-03): add email-config admin routes + D-13 read-side visibility filter + EmailAlertConfigForm
buildTenderWhere gains an optional ownerTenantId param: a resolved
requesting tenant sees global tenders (null) plus its own private ones
(OR[global, mine]); an unresolved requester fails CLOSED to global-only —
never an accidental cross-tenant leak.
TendersController: listTenders/getTender resolve the requesting tenant
leniently from the auth context (resolveRequestingTenantId, never throws)
and apply the D-13 filter; getTender 404s (not a distinct "forbidden") when
a tender's non-null ownerTenantId doesn't match the requester, so no
cross-tenant detail leak. New GET/PUT /modules/tender-radar/email-config
routes (Roles ADMIN/SUPER_ADMIN, tenantId from auth context, never the
body) delegate to TenderEmailConfigService — declared before @Get(':id')
per the project's NestJS route-order convention.
Web: EmailAlertConfig type + fetchEmailConfig/saveEmailConfig client
functions; EmailAlertConfigForm mirrors the DKV InboxConfigForm (password
blank on load, only sent when typed — T-07-12), added as a new
"E-Mail-Alerts" section on the existing tender-radar settings page.
Hardcoded German strings — i18n is Plan 14-05.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -446,3 +446,68 @@ export async function deleteRssFeed(id: string): Promise<void> {
|
||||
throw new Error(await extractErrorMessage(res, 'Failed to delete RSS feed'));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This tenant's portal-alert mailbox config (Plan 14-03, INGEST-05/
|
||||
* CONFIG-02, D-06/D-07). Unlike SourceConfig/RssFeedSource (platform-wide),
|
||||
* this is PER-TENANT — the backend derives tenantId from the auth cookie,
|
||||
* this client never sends a tenantId.
|
||||
*
|
||||
* Security (T-07-12): the password is NEVER returned — only `hasPassword`.
|
||||
* The password field is only sent in `saveEmailConfig`'s payload when the
|
||||
* admin has typed a new one (same InboxConfigForm/DKV convention).
|
||||
*/
|
||||
export interface EmailAlertConfig {
|
||||
protocol: 'imap' | 'exchange';
|
||||
host: string | null;
|
||||
port: number | null;
|
||||
encryption: 'none' | 'starttls' | 'ssl-tls';
|
||||
folder: string;
|
||||
senderFilter?: string | null;
|
||||
domain?: string | null;
|
||||
isActive: boolean;
|
||||
username?: string | null;
|
||||
/** true if a password is stored server-side — never the actual secret */
|
||||
hasPassword: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch this tenant's email-alert mailbox config. Returns null when no
|
||||
* config has been saved yet (fresh tenant — mirrors DkvConfig's fetchConfig
|
||||
* convention).
|
||||
* GET /modules/tender-radar/email-config
|
||||
*/
|
||||
export async function fetchEmailConfig(): Promise<EmailAlertConfig | null> {
|
||||
const res = await fetch(`${API_URL}/modules/tender-radar/email-config`, {
|
||||
credentials: 'include',
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
await extractErrorMessage(res, 'Failed to fetch email-alert config'),
|
||||
);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* Save this tenant's email-alert mailbox config.
|
||||
* PUT /modules/tender-radar/email-config
|
||||
* Only include `password` in the payload when the admin typed a new one
|
||||
* (T-07-12 — blank-on-load convention).
|
||||
*/
|
||||
export async function saveEmailConfig(
|
||||
payload: Partial<EmailAlertConfig> & { password?: string },
|
||||
): Promise<EmailAlertConfig> {
|
||||
const res = await fetch(`${API_URL}/modules/tender-radar/email-config`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
credentials: 'include',
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (!res.ok) {
|
||||
throw new Error(
|
||||
await extractErrorMessage(res, 'Failed to save email-alert config'),
|
||||
);
|
||||
}
|
||||
return res.json();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user