114 lines
3.0 KiB
TypeScript
114 lines
3.0 KiB
TypeScript
import { type ActionFunction } from "@remix-run/node";
|
|
import { badRequest, notFound } from "remix-utils";
|
|
import { z } from "zod";
|
|
|
|
import db from "~/utils/db.server";
|
|
import logger from "~/utils/logger.server";
|
|
import { validate } from "~/utils/validation.server";
|
|
import { requireLoggedIn } from "~/utils/auth.server";
|
|
|
|
const action: ActionFunction = async ({ request }) => {
|
|
const formData = await request.clone().formData();
|
|
const action = formData.get("_action");
|
|
if (!action) {
|
|
const errorMessage = "POST /notifications-subscription without any _action";
|
|
logger.error(errorMessage);
|
|
return badRequest({ errorMessage });
|
|
}
|
|
|
|
switch (action as Action) {
|
|
case "subscribe":
|
|
return subscribe(request);
|
|
case "unsubscribe":
|
|
return unsubscribe(request);
|
|
default:
|
|
const errorMessage = `POST /notifications-subscription with an invalid _action=${action}`;
|
|
logger.error(errorMessage);
|
|
return badRequest({ errorMessage });
|
|
}
|
|
};
|
|
|
|
export default action;
|
|
|
|
async function subscribe(request: Request) {
|
|
const { organization } = await requireLoggedIn(request);
|
|
const formData = await request.formData();
|
|
const body = {
|
|
subscription: JSON.parse(formData.get("subscription")?.toString() ?? "{}"),
|
|
};
|
|
const validation = validate(validations.subscribe, body);
|
|
if (validation.errors) {
|
|
return badRequest(validation.errors);
|
|
}
|
|
|
|
const { subscription } = validation.data;
|
|
const membership = await db.membership.findFirst({
|
|
where: { id: organization.membershipId },
|
|
});
|
|
if (!membership) {
|
|
return notFound("Phone number not found");
|
|
}
|
|
|
|
try {
|
|
await db.notificationSubscription.create({
|
|
data: {
|
|
membershipId: membership.id,
|
|
endpoint: subscription.endpoint,
|
|
expirationTime: subscription.expirationTime,
|
|
keys_p256dh: subscription.keys.p256dh,
|
|
keys_auth: subscription.keys.auth,
|
|
},
|
|
});
|
|
} catch (error: any) {
|
|
if (error.code !== "P2002") {
|
|
throw error;
|
|
}
|
|
|
|
logger.warn(`Duplicate insertion of subscription with endpoint=${subscription.endpoint}`);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
async function unsubscribe(request: Request) {
|
|
const formData = await request.formData();
|
|
const body = {
|
|
subscriptionEndpoint: formData.get("subscriptionEndpoint"),
|
|
};
|
|
const validation = validate(validations.unsubscribe, body);
|
|
if (validation.errors) {
|
|
return badRequest(validation.errors);
|
|
}
|
|
|
|
const endpoint = validation.data.subscriptionEndpoint;
|
|
try {
|
|
await db.notificationSubscription.delete({ where: { endpoint } });
|
|
} catch (error: any) {
|
|
if (error.code !== "P2025") {
|
|
throw error;
|
|
}
|
|
|
|
logger.warn(`Could not delete subscription with endpoint=${endpoint} because it has already been deleted`);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
type Action = "subscribe" | "unsubscribe";
|
|
|
|
const validations = {
|
|
subscribe: z.object({
|
|
subscription: z.object({
|
|
endpoint: z.string(),
|
|
expirationTime: z.number().nullable(),
|
|
keys: z.object({
|
|
p256dh: z.string(),
|
|
auth: z.string(),
|
|
}),
|
|
}),
|
|
}),
|
|
unsubscribe: z.object({
|
|
subscriptionEndpoint: z.string(),
|
|
}),
|
|
} as const;
|