feat(notifications): implement notification router with email, push, SMS support
- Add notification router (sendEmail, sendPush, sendSMS, device mgmt, prefs) - Create provider clients: Resend, Firebase Admin (FCM), Twilio - Add notification_preferences table to Drizzle schema - Create branded email templates (welcome, alert, password reset, family invite, billing) - Implement notification service with error handling and E.164 validation - Wire router into app root - Write unit tests with mocked providers (25 tests passing) - Add resend, firebase-admin, twilio dependencies
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import { exampleRouter } from "./routers/example";
|
||||
import { userRouter } from "./routers/user";
|
||||
import { billingRouter } from "./routers/billing";
|
||||
import { notificationRouter } from "./routers/notification";
|
||||
import { createTRPCRouter } from "./utils";
|
||||
|
||||
export const appRouter = createTRPCRouter({
|
||||
example: exampleRouter,
|
||||
user: userRouter,
|
||||
billing: billingRouter,
|
||||
notification: notificationRouter,
|
||||
});
|
||||
|
||||
export type AppRouter = typeof appRouter;
|
||||
|
||||
100
web/src/server/api/routers/notification.ts
Normal file
100
web/src/server/api/routers/notification.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { wrap } from "@typeschema/valibot";
|
||||
import {
|
||||
object,
|
||||
string,
|
||||
optional,
|
||||
record,
|
||||
boolean,
|
||||
picklist,
|
||||
} from "valibot";
|
||||
import { createTRPCRouter, protectedProcedure, adminProcedure } from "../utils";
|
||||
import {
|
||||
sendEmail,
|
||||
sendPush,
|
||||
sendSMS,
|
||||
registerDevice,
|
||||
unregisterDevice,
|
||||
listDevices,
|
||||
getPreferences,
|
||||
updatePreferences,
|
||||
} from "~/server/services/notification.service";
|
||||
|
||||
const SendEmailSchema = object({
|
||||
to: string(),
|
||||
subject: string(),
|
||||
html: string(),
|
||||
text: optional(string()),
|
||||
});
|
||||
|
||||
const SendPushSchema = object({
|
||||
title: string(),
|
||||
body: string(),
|
||||
data: optional(record(string(), string())),
|
||||
});
|
||||
|
||||
const SendSMSSchema = object({
|
||||
phoneNumber: string(),
|
||||
message: string(),
|
||||
});
|
||||
|
||||
const RegisterDeviceSchema = object({
|
||||
token: string(),
|
||||
platform: picklist(["ios", "android", "web"]),
|
||||
deviceType: picklist(["mobile", "web", "desktop"]),
|
||||
});
|
||||
|
||||
const UnregisterDeviceSchema = object({
|
||||
token: string(),
|
||||
});
|
||||
|
||||
const UpdatePreferencesSchema = object({
|
||||
emailEnabled: optional(boolean()),
|
||||
pushEnabled: optional(boolean()),
|
||||
smsEnabled: optional(boolean()),
|
||||
});
|
||||
|
||||
export const notificationRouter = createTRPCRouter({
|
||||
sendEmail: adminProcedure
|
||||
.input(wrap(SendEmailSchema))
|
||||
.mutation(async ({ input }) => {
|
||||
return sendEmail(input.to, input.subject, input.html, input.text);
|
||||
}),
|
||||
|
||||
sendPush: protectedProcedure
|
||||
.input(wrap(SendPushSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return sendPush(ctx.user.id, input.title, input.body, input.data);
|
||||
}),
|
||||
|
||||
sendSMS: protectedProcedure
|
||||
.input(wrap(SendSMSSchema))
|
||||
.mutation(async ({ input }) => {
|
||||
return sendSMS(input.phoneNumber, input.message);
|
||||
}),
|
||||
|
||||
registerDevice: protectedProcedure
|
||||
.input(wrap(RegisterDeviceSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return registerDevice(ctx.user.id, input.token, input.platform, input.deviceType);
|
||||
}),
|
||||
|
||||
unregisterDevice: protectedProcedure
|
||||
.input(wrap(UnregisterDeviceSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return unregisterDevice(ctx.user.id, input.token);
|
||||
}),
|
||||
|
||||
listDevices: protectedProcedure.query(async ({ ctx }) => {
|
||||
return listDevices(ctx.user.id);
|
||||
}),
|
||||
|
||||
getPreferences: protectedProcedure.query(async ({ ctx }) => {
|
||||
return getPreferences(ctx.user.id);
|
||||
}),
|
||||
|
||||
updatePreferences: protectedProcedure
|
||||
.input(wrap(UpdatePreferencesSchema))
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
return updatePreferences(ctx.user.id, input);
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user