mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-15 06:52:04 +00:00
feat: create managed profiles atomically
- Require a fresh cookie MFA proof before creation\n- Create the credential-less profile and initial Guardian grant together
This commit is contained in:
1 parent
51cbbae17f
commit
58434a2b44
5 files changed
+171
-11
No files matched your search
@@ -0,0 +1,88 @@
|
||||
import { randomBytes } from "node:crypto";
|
||||
|
||||
import { Prisma } from "@/generated/prisma/client";
|
||||
import type { AccountGrant, User } from "@/generated/prisma/client";
|
||||
import { prisma } from "@/lib/db";
|
||||
|
||||
const USERNAME_ATTEMPTS = 4;
|
||||
|
||||
export interface CreateManagedProfileInput {
|
||||
creatorId: string;
|
||||
displayName: string;
|
||||
dateOfBirth: Date | null;
|
||||
locale: string;
|
||||
timezone: string;
|
||||
}
|
||||
|
||||
export interface CreatedManagedProfile {
|
||||
profile: User;
|
||||
creatorGrant: AccountGrant;
|
||||
}
|
||||
|
||||
export class ManagedProfileCreationError extends Error {
|
||||
constructor() {
|
||||
super("Unable to allocate a managed profile identity");
|
||||
this.name = "ManagedProfileCreationError";
|
||||
}
|
||||
}
|
||||
|
||||
function managedUsername(): string {
|
||||
return `managed-${randomBytes(18).toString("hex")}`;
|
||||
}
|
||||
|
||||
function isUniqueViolation(error: unknown): boolean {
|
||||
return (
|
||||
error instanceof Prisma.PrismaClientKnownRequestError &&
|
||||
error.code === "P2002"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create the credential-less record and its first Guardian grant together.
|
||||
*
|
||||
* A profile is never useful before the creator can manage it, and a creator
|
||||
* grant without its record is invalid by definition. Keeping both writes in
|
||||
* one transaction makes a failed grant insert leave no login-less user behind.
|
||||
*/
|
||||
export async function createManagedProfile(
|
||||
input: CreateManagedProfileInput,
|
||||
): Promise<CreatedManagedProfile> {
|
||||
for (let attempt = 0; attempt < USERNAME_ATTEMPTS; attempt += 1) {
|
||||
try {
|
||||
return await prisma.$transaction(async (tx) => {
|
||||
const now = new Date();
|
||||
const profile = await tx.user.create({
|
||||
data: {
|
||||
username: managedUsername(),
|
||||
email: null,
|
||||
passwordHash: null,
|
||||
displayName: input.displayName,
|
||||
dateOfBirth: input.dateOfBirth,
|
||||
locale: input.locale,
|
||||
timezone: input.timezone,
|
||||
managedProfileAt: now,
|
||||
},
|
||||
});
|
||||
const creatorGrant = await tx.accountGrant.create({
|
||||
data: {
|
||||
grantorId: profile.id,
|
||||
granteeId: input.creatorId,
|
||||
access: "MANAGE",
|
||||
scopeJson: Prisma.DbNull,
|
||||
invitedAt: now,
|
||||
acceptedAt: now,
|
||||
expiresAt: null,
|
||||
},
|
||||
});
|
||||
return { profile, creatorGrant };
|
||||
});
|
||||
} catch (error) {
|
||||
if (isUniqueViolation(error) && attempt + 1 < USERNAME_ATTEMPTS) {
|
||||
continue;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new ManagedProfileCreationError();
|
||||
}
|
||||
Reference in new issue
Block a user