#36 CRITICAL: add .max(128) to all password Zod schemas to prevent Argon2-based DoS from unbounded password strings. #46 HIGH: configure pino redact paths so passwords/tokens/cookies/TOTP secrets are never serialized in logs. #58 MEDIUM: upgrade dompurify to ^3.4.0 and add pnpm overrides for brace-expansion (>=5.0.5) and esbuild (>=0.25.0) to patch known CVEs. Vite moderate (path traversal, dev-only) remains — requires vitest 3.x major upgrade, deferred. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -74,7 +74,7 @@ export const authRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
token: z.string().min(1),
|
||||
password: z.string().min(12, "Password must be at least 12 characters."),
|
||||
password: z.string().min(12, "Password must be at least 12 characters.").max(128),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
|
||||
@@ -99,8 +99,10 @@ export const inviteRouter = createTRPCRouter({
|
||||
select: { email: true, role: true, expiresAt: true, usedAt: true },
|
||||
});
|
||||
if (!invite) throw new TRPCError({ code: "NOT_FOUND", message: "Invite not found." });
|
||||
if (invite.usedAt) throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has already been used." });
|
||||
if (invite.expiresAt < new Date()) throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has expired." });
|
||||
if (invite.usedAt)
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has already been used." });
|
||||
if (invite.expiresAt < new Date())
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has expired." });
|
||||
return { email: invite.email, role: invite.role };
|
||||
}),
|
||||
|
||||
@@ -109,7 +111,7 @@ export const inviteRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
token: z.string(),
|
||||
password: z.string().min(12, "Password must be at least 12 characters."),
|
||||
password: z.string().min(12, "Password must be at least 12 characters.").max(128),
|
||||
}),
|
||||
)
|
||||
.mutation(async ({ ctx, input }) => {
|
||||
@@ -125,13 +127,18 @@ export const inviteRouter = createTRPCRouter({
|
||||
where: { token: input.token },
|
||||
});
|
||||
if (!invite) throw new TRPCError({ code: "NOT_FOUND", message: "Invite not found." });
|
||||
if (invite.usedAt) throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has already been used." });
|
||||
if (invite.expiresAt < new Date()) throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has expired." });
|
||||
if (invite.usedAt)
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has already been used." });
|
||||
if (invite.expiresAt < new Date())
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "This invite has expired." });
|
||||
|
||||
// Check if user already exists
|
||||
const existing = await ctx.db.user.findUnique({ where: { email: invite.email } });
|
||||
if (existing) {
|
||||
throw new TRPCError({ code: "CONFLICT", message: "An account with this email already exists." });
|
||||
throw new TRPCError({
|
||||
code: "CONFLICT",
|
||||
message: "An account with this email already exists.",
|
||||
});
|
||||
}
|
||||
|
||||
const { hash } = await import("@node-rs/argon2");
|
||||
|
||||
@@ -10,12 +10,12 @@ export const CreateUserInputSchema = z.object({
|
||||
email: z.string().email(),
|
||||
name: z.string().min(1),
|
||||
systemRole: z.nativeEnum(SystemRole).default(SystemRole.USER),
|
||||
password: z.string().min(12),
|
||||
password: z.string().min(12).max(128),
|
||||
});
|
||||
|
||||
export const SetUserPasswordInputSchema = z.object({
|
||||
userId: z.string(),
|
||||
password: z.string().min(12, "Password must be at least 12 characters"),
|
||||
password: z.string().min(12, "Password must be at least 12 characters").max(128),
|
||||
});
|
||||
|
||||
export const UpdateUserRoleInputSchema = z.object({
|
||||
@@ -289,10 +289,7 @@ export async function linkUserResource(
|
||||
const linkResult = await ctx.db.resource.updateMany({
|
||||
where: {
|
||||
id: input.resourceId,
|
||||
OR: [
|
||||
{ userId: null },
|
||||
{ userId: input.userId },
|
||||
],
|
||||
OR: [{ userId: null }, { userId: input.userId }],
|
||||
},
|
||||
data: { userId: input.userId },
|
||||
});
|
||||
@@ -393,7 +390,10 @@ export async function setUserPermissions(
|
||||
entityId: input.userId,
|
||||
entityName: `${before.name} (${before.email})`,
|
||||
action: "UPDATE",
|
||||
before: { permissionOverrides: before.permissionOverrides } as unknown as Record<string, unknown>,
|
||||
before: { permissionOverrides: before.permissionOverrides } as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>,
|
||||
after: { permissionOverrides: input.overrides } as unknown as Record<string, unknown>,
|
||||
summary: input.overrides
|
||||
? `Set permission overrides (granted: ${input.overrides.granted?.length ?? 0}, denied: ${input.overrides.denied?.length ?? 0})`
|
||||
@@ -427,7 +427,10 @@ export async function resetUserPermissions(
|
||||
entityId: input.userId,
|
||||
entityName: `${before.name} (${before.email})`,
|
||||
action: "UPDATE",
|
||||
before: { permissionOverrides: before.permissionOverrides } as unknown as Record<string, unknown>,
|
||||
before: { permissionOverrides: before.permissionOverrides } as unknown as Record<
|
||||
string,
|
||||
unknown
|
||||
>,
|
||||
after: { permissionOverrides: null } as unknown as Record<string, unknown>,
|
||||
summary: "Reset permission overrides to role defaults",
|
||||
});
|
||||
@@ -464,7 +467,10 @@ export async function deactivateUser(
|
||||
) {
|
||||
const audit = makeAuditLogger(ctx.db, ctx.dbUser?.id);
|
||||
if (ctx.dbUser!.id === input.userId) {
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "You cannot deactivate your own account." });
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "You cannot deactivate your own account.",
|
||||
});
|
||||
}
|
||||
|
||||
const user = await findUniqueOrThrow(
|
||||
@@ -479,7 +485,10 @@ export async function deactivateUser(
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "User is already inactive." });
|
||||
}
|
||||
|
||||
await ctx.db.user.update({ where: { id: input.userId }, data: { isActive: false, deletedAt: new Date() } });
|
||||
await ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
data: { isActive: false, deletedAt: new Date() },
|
||||
});
|
||||
|
||||
// Invalidate all existing sessions so the user is logged out immediately
|
||||
await ctx.db.activeSession.deleteMany({ where: { userId: input.userId } });
|
||||
@@ -512,7 +521,10 @@ export async function reactivateUser(
|
||||
throw new TRPCError({ code: "BAD_REQUEST", message: "User is already active." });
|
||||
}
|
||||
|
||||
await ctx.db.user.update({ where: { id: input.userId }, data: { isActive: true, deletedAt: null } });
|
||||
await ctx.db.user.update({
|
||||
where: { id: input.userId },
|
||||
data: { isActive: true, deletedAt: null },
|
||||
});
|
||||
|
||||
audit({
|
||||
entityType: "User",
|
||||
|
||||
Reference in New Issue
Block a user