Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 | 15x 15x 16x 9x 1x 8x 8x 8x 8x 6x 6x 2x 2x 1x 1x 1x 4x 4x 1x 3x 3x | import { z } from "zod";
import type { Invitation } from "@/types";
import { randomBytes } from "crypto";
import { normalizeEmail } from "@/lib/normalize-email";
import type { Database } from "@/lib/db";
import { InvitationRepository } from "@/server/repositories/implementations/InvitationRepository";
import { sendInvitation } from "@/lib/email";
const CreateInvitationSchema = z.object({
email: z.string().email(),
});
// Invitation flags are inert (DB-default only) and not mutable via PATCH — the
// repo update allow-list writes only status/acceptedAt. An empty schema keeps the
// member side and invitation side symmetric: an unknown flag key is stripped, not
// a 400. (Flag columns/fields are dropped in the deferred contract migration.)
const UpdateInvitationSchema = z.object({});
export class InvitationController {
constructor(private invitationRepo: InvitationRepository) {}
static fromDb(db: Database): InvitationController {
return new InvitationController(new InvitationRepository(db));
}
async listByCollection(collectionId: string): Promise<Invitation[]> {
return this.invitationRepo.findByCollection(collectionId);
}
async createInvitation(
collectionId: string,
data: unknown,
): Promise<Invitation & { token: string; emailSent: boolean }> {
const parsed = CreateInvitationSchema.parse(data);
const email = normalizeEmail(parsed.email);
const token = randomBytes(32).toString("hex");
// Flags are not accepted from callers — DB column defaults govern all permissions.
const invitation = await this.invitationRepo.create({
collectionId,
email,
token,
});
// The invitation itself is always created — email is best-effort delivery,
// so a transport miss (e.g. no RESEND_API_KEY) never fails the request.
const { sent } = await sendInvitation({ to: email, token });
return { ...invitation, token, emailSent: sent };
}
async getInvitation(invitationId: string): Promise<Invitation> {
const invitation = await this.invitationRepo.findById(invitationId);
if (!invitation) {
throw new Error("Invitation not found");
}
return invitation;
}
async updateInvitation(
invitationId: string,
data: unknown,
): Promise<Invitation> {
const parsed = UpdateInvitationSchema.parse(data);
return this.invitationRepo.update(invitationId, parsed);
}
async revokeInvitation(invitationId: string): Promise<void> {
const invitation = await this.invitationRepo.findById(invitationId);
Iif (!invitation) {
throw new Error("Invitation not found");
}
await this.invitationRepo.update(invitationId, { status: "revoked" });
}
async resendInvitation(invitationId: string): Promise<{ message: string; emailSent: boolean }> {
const invitation = await this.invitationRepo.findById(invitationId);
if (!invitation) {
throw new Error("Invitation not found");
}
const { sent } = await sendInvitation({ to: invitation.email, token: invitation.token });
return { message: "Invitation resent", emailSent: sent };
}
}
|