Files
ReadMeABook/src/app/api/admin/notifications/[id]/route.ts
T
kikootwo dc7e557694 Add notification system with admin UI and backend
Introduces a full notification system with support for Discord and Pushover backends, event triggers, and message formatting. Adds backend services, processors, and API endpoints for managing notifications, as well as a new Notifications tab in the admin settings UI. Updates documentation, database schema, and tests to cover notification features and approval workflow improvements. Also changes project license from MIT to AGPL v3.
2026-01-28 11:42:00 -05:00

227 lines
6.6 KiB
TypeScript

/**
* Component: Notification Backend Individual API
* Documentation: documentation/backend/services/notifications.md
*/
import { NextRequest, NextResponse } from 'next/server';
import { requireAuth, requireAdmin, AuthenticatedRequest } from '@/lib/middleware/auth';
import { prisma } from '@/lib/db';
import { getNotificationService, NotificationBackendType } from '@/lib/services/notification.service';
import { RMABLogger } from '@/lib/utils/logger';
import { z } from 'zod';
const logger = RMABLogger.create('API.Admin.Notifications.Id');
const UpdateBackendSchema = z.object({
name: z.string().min(1).optional(),
config: z.record(z.any()).optional(),
events: z.array(z.enum(['request_pending_approval', 'request_approved', 'request_available', 'request_error'])).min(1).optional(),
enabled: z.boolean().optional(),
});
/**
* GET /api/admin/notifications/[id]
* Get single notification backend (sensitive values masked)
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { id } = await params;
const backend = await prisma.notificationBackend.findUnique({
where: { id },
});
if (!backend) {
return NextResponse.json(
{ error: 'NotFound', message: 'Notification backend not found' },
{ status: 404 }
);
}
const notificationService = getNotificationService();
// Mask sensitive config values
return NextResponse.json({
success: true,
backend: {
...backend,
config: notificationService.maskConfig(backend.type as NotificationBackendType, backend.config),
},
});
} catch (error) {
logger.error('Failed to fetch notification backend', {
error: error instanceof Error ? error.message : String(error),
});
return NextResponse.json(
{
error: 'FetchError',
message: 'Failed to fetch notification backend',
},
{ status: 500 }
);
}
});
});
}
/**
* PUT /api/admin/notifications/[id]
* Update notification backend
*/
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { id } = await params;
const body = await request.json();
const updates = UpdateBackendSchema.parse(body);
// Get existing backend
const existing = await prisma.notificationBackend.findUnique({
where: { id },
});
if (!existing) {
return NextResponse.json(
{ error: 'NotFound', message: 'Notification backend not found' },
{ status: 404 }
);
}
const notificationService = getNotificationService();
// Handle config updates (preserve masked values, encrypt new values)
let finalConfig = existing.config;
if (updates.config) {
const existingConfig = existing.config as any;
const updatedConfig = updates.config as any;
// Check if masked values need to be preserved
Object.keys(updatedConfig).forEach((key) => {
if (updatedConfig[key] === '••••••••') {
// Preserve existing encrypted value
updatedConfig[key] = existingConfig[key];
}
});
// Encrypt new/changed values
finalConfig = notificationService.encryptConfig(existing.type as NotificationBackendType, updatedConfig);
}
// Update backend
const updateData: any = {};
if (updates.name) updateData.name = updates.name;
if (updates.config) updateData.config = finalConfig;
if (updates.events) updateData.events = updates.events;
if (updates.enabled !== undefined) updateData.enabled = updates.enabled;
const updated = await prisma.notificationBackend.update({
where: { id },
data: updateData,
});
logger.info(`Updated notification backend: ${updated.name}`, {
backendId: id,
adminId: req.user?.sub,
});
// Return with masked values
return NextResponse.json({
success: true,
backend: {
...updated,
config: notificationService.maskConfig(updated.type as NotificationBackendType, updated.config),
},
});
} catch (error) {
logger.error('Failed to update notification backend', {
error: error instanceof Error ? error.message : String(error),
});
if (error instanceof z.ZodError) {
return NextResponse.json(
{
error: 'ValidationError',
details: error.errors,
},
{ status: 400 }
);
}
return NextResponse.json(
{
error: 'UpdateError',
message: 'Failed to update notification backend',
},
{ status: 500 }
);
}
});
});
}
/**
* DELETE /api/admin/notifications/[id]
* Delete notification backend
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
return requireAuth(request, async (req: AuthenticatedRequest) => {
return requireAdmin(req, async () => {
try {
const { id } = await params;
// Check if backend exists
const backend = await prisma.notificationBackend.findUnique({
where: { id },
});
if (!backend) {
return NextResponse.json(
{ error: 'NotFound', message: 'Notification backend not found' },
{ status: 404 }
);
}
// Delete backend
await prisma.notificationBackend.delete({
where: { id },
});
logger.info(`Deleted notification backend: ${backend.name}`, {
backendId: id,
adminId: req.user?.sub,
});
return NextResponse.json({
success: true,
message: 'Notification backend deleted',
});
} catch (error) {
logger.error('Failed to delete notification backend', {
error: error instanceof Error ? error.message : String(error),
});
return NextResponse.json(
{
error: 'DeleteError',
message: 'Failed to delete notification backend',
},
{ status: 500 }
);
}
});
});
}