chore: enable Gotify and Custom Webhhok notifications and improve payload validation
- Enhanced Notifications component tests to include support for Discord, Gotify, and Webhook provider types. - Updated test cases to validate the correct handling of provider type options and ensure proper payload structure during creation, preview, and testing. - Introduced new tests for Gotify token handling and ensured sensitive information is not exposed in the UI. - Refactored existing tests for clarity and maintainability, including improved assertions and error handling. - Added comprehensive coverage for payload validation scenarios, including malformed requests and security checks against SSRF and oversized payloads.
This commit is contained in:
@@ -1,6 +1,24 @@
|
||||
import client from './client';
|
||||
|
||||
const DISCORD_PROVIDER_TYPE = 'discord' as const;
|
||||
export const SUPPORTED_NOTIFICATION_PROVIDER_TYPES = ['discord', 'gotify', 'webhook'] as const;
|
||||
export type SupportedNotificationProviderType = (typeof SUPPORTED_NOTIFICATION_PROVIDER_TYPES)[number];
|
||||
const DEFAULT_PROVIDER_TYPE: SupportedNotificationProviderType = 'discord';
|
||||
|
||||
const isSupportedNotificationProviderType = (type: string | undefined): type is SupportedNotificationProviderType =>
|
||||
typeof type === 'string' && SUPPORTED_NOTIFICATION_PROVIDER_TYPES.includes(type.toLowerCase() as SupportedNotificationProviderType);
|
||||
|
||||
const resolveProviderTypeOrThrow = (type: string | undefined): SupportedNotificationProviderType => {
|
||||
if (typeof type === 'undefined') {
|
||||
return DEFAULT_PROVIDER_TYPE;
|
||||
}
|
||||
|
||||
const normalizedType = type.toLowerCase();
|
||||
if (isSupportedNotificationProviderType(normalizedType)) {
|
||||
return normalizedType;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported notification provider type: ${type}`);
|
||||
};
|
||||
|
||||
/** Notification provider configuration. */
|
||||
export interface NotificationProvider {
|
||||
@@ -10,6 +28,8 @@ export interface NotificationProvider {
|
||||
url: string;
|
||||
config?: string;
|
||||
template?: string;
|
||||
gotify_token?: string;
|
||||
token?: string;
|
||||
enabled: boolean;
|
||||
notify_proxy_hosts: boolean;
|
||||
notify_remote_servers: boolean;
|
||||
@@ -23,19 +43,39 @@ export interface NotificationProvider {
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const withDiscordType = (data: Partial<NotificationProvider>): Partial<NotificationProvider> => {
|
||||
const normalizedType = typeof data.type === 'string' ? data.type.toLowerCase() : undefined;
|
||||
if (normalizedType !== DISCORD_PROVIDER_TYPE) {
|
||||
return { ...data, type: DISCORD_PROVIDER_TYPE };
|
||||
const sanitizeProviderForWriteAction = (data: Partial<NotificationProvider>): Partial<NotificationProvider> => {
|
||||
const type = resolveProviderTypeOrThrow(data.type);
|
||||
const payload: Partial<NotificationProvider> = {
|
||||
...data,
|
||||
type,
|
||||
};
|
||||
|
||||
const normalizedToken = typeof payload.gotify_token === 'string' && payload.gotify_token.trim().length > 0
|
||||
? payload.gotify_token.trim()
|
||||
: typeof payload.token === 'string' && payload.token.trim().length > 0
|
||||
? payload.token.trim()
|
||||
: undefined;
|
||||
|
||||
delete payload.gotify_token;
|
||||
|
||||
if (type !== 'gotify') {
|
||||
delete payload.token;
|
||||
return payload;
|
||||
}
|
||||
|
||||
return { ...data, type: DISCORD_PROVIDER_TYPE };
|
||||
if (normalizedToken) {
|
||||
payload.token = normalizedToken;
|
||||
} else {
|
||||
delete payload.token;
|
||||
}
|
||||
|
||||
return payload;
|
||||
};
|
||||
|
||||
const assertDiscordOnlyInput = (data: Partial<NotificationProvider>): void => {
|
||||
if (typeof data.type === 'string' && data.type.toLowerCase() !== DISCORD_PROVIDER_TYPE) {
|
||||
throw new Error('Only discord notification providers are supported');
|
||||
}
|
||||
const sanitizeProviderForReadLikeAction = (data: Partial<NotificationProvider>): Partial<NotificationProvider> => {
|
||||
const payload = sanitizeProviderForWriteAction(data);
|
||||
delete payload.token;
|
||||
return payload;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -55,8 +95,7 @@ export const getProviders = async () => {
|
||||
* @throws {AxiosError} If creation fails
|
||||
*/
|
||||
export const createProvider = async (data: Partial<NotificationProvider>) => {
|
||||
assertDiscordOnlyInput(data);
|
||||
const response = await client.post<NotificationProvider>('/notifications/providers', withDiscordType(data));
|
||||
const response = await client.post<NotificationProvider>('/notifications/providers', sanitizeProviderForWriteAction(data));
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -68,8 +107,7 @@ export const createProvider = async (data: Partial<NotificationProvider>) => {
|
||||
* @throws {AxiosError} If update fails or provider not found
|
||||
*/
|
||||
export const updateProvider = async (id: string, data: Partial<NotificationProvider>) => {
|
||||
assertDiscordOnlyInput(data);
|
||||
const response = await client.put<NotificationProvider>(`/notifications/providers/${id}`, withDiscordType(data));
|
||||
const response = await client.put<NotificationProvider>(`/notifications/providers/${id}`, sanitizeProviderForWriteAction(data));
|
||||
return response.data;
|
||||
};
|
||||
|
||||
@@ -88,8 +126,7 @@ export const deleteProvider = async (id: string) => {
|
||||
* @throws {AxiosError} If test fails
|
||||
*/
|
||||
export const testProvider = async (provider: Partial<NotificationProvider>) => {
|
||||
assertDiscordOnlyInput(provider);
|
||||
await client.post('/notifications/providers/test', withDiscordType(provider));
|
||||
await client.post('/notifications/providers/test', sanitizeProviderForReadLikeAction(provider));
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -116,8 +153,7 @@ export interface NotificationTemplate {
|
||||
* @throws {AxiosError} If preview fails
|
||||
*/
|
||||
export const previewProvider = async (provider: Partial<NotificationProvider>, data?: Record<string, unknown>) => {
|
||||
assertDiscordOnlyInput(provider);
|
||||
const payload: Record<string, unknown> = withDiscordType(provider) as Record<string, unknown>;
|
||||
const payload: Record<string, unknown> = sanitizeProviderForReadLikeAction(provider) as Record<string, unknown>;
|
||||
if (data) payload.data = data;
|
||||
const response = await client.post('/notifications/providers/preview', payload);
|
||||
return response.data;
|
||||
|
||||
Reference in New Issue
Block a user