undone
This commit is contained in:
@@ -7,13 +7,6 @@ import { ConnectionFactory, hashPassword, checkPassword } from "~/server/utils";
|
||||
import { SignJWT, jwtVerify } from "jose";
|
||||
import { setCookie, getCookie } from "vinxi/http";
|
||||
import type { User } from "~/types/user";
|
||||
import {
|
||||
emailSchema,
|
||||
passwordSchema,
|
||||
registrationSchema,
|
||||
loginSchema,
|
||||
passwordResetSchema
|
||||
} from "~/server/api/schemas/validation";
|
||||
|
||||
// Helper to create JWT token
|
||||
async function createJWT(
|
||||
@@ -246,7 +239,7 @@ export const authRouter = createTRPCRouter({
|
||||
emailLogin: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: emailSchema,
|
||||
email: z.string().email(),
|
||||
token: z.string(),
|
||||
rememberMe: z.boolean().optional()
|
||||
})
|
||||
@@ -324,7 +317,7 @@ export const authRouter = createTRPCRouter({
|
||||
emailVerification: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: emailSchema,
|
||||
email: z.string().email(),
|
||||
token: z.string()
|
||||
})
|
||||
)
|
||||
@@ -367,9 +360,22 @@ export const authRouter = createTRPCRouter({
|
||||
|
||||
// Email/password registration
|
||||
emailRegistration: publicProcedure
|
||||
.input(registrationSchema)
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string().min(8),
|
||||
passwordConfirmation: z.string().min(8)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { email, password } = input;
|
||||
const { email, password, passwordConfirmation } = input;
|
||||
|
||||
if (password !== passwordConfirmation) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "passwordMismatch"
|
||||
});
|
||||
}
|
||||
|
||||
const passwordHash = await hashPassword(password);
|
||||
const conn = ConnectionFactory();
|
||||
@@ -405,7 +411,13 @@ export const authRouter = createTRPCRouter({
|
||||
|
||||
// Email/password login
|
||||
emailPasswordLogin: publicProcedure
|
||||
.input(loginSchema)
|
||||
.input(
|
||||
z.object({
|
||||
email: z.string().email(),
|
||||
password: z.string(),
|
||||
rememberMe: z.boolean().optional()
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { email, password, rememberMe } = input;
|
||||
|
||||
@@ -465,7 +477,7 @@ export const authRouter = createTRPCRouter({
|
||||
requestEmailLinkLogin: publicProcedure
|
||||
.input(
|
||||
z.object({
|
||||
email: emailSchema,
|
||||
email: z.string().email(),
|
||||
rememberMe: z.boolean().optional()
|
||||
})
|
||||
)
|
||||
@@ -570,7 +582,7 @@ export const authRouter = createTRPCRouter({
|
||||
|
||||
// Request password reset
|
||||
requestPasswordReset: publicProcedure
|
||||
.input(z.object({ email: emailSchema }))
|
||||
.input(z.object({ email: z.string().email() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { email } = input;
|
||||
|
||||
@@ -669,9 +681,22 @@ export const authRouter = createTRPCRouter({
|
||||
|
||||
// Reset password with token
|
||||
resetPassword: publicProcedure
|
||||
.input(passwordResetSchema)
|
||||
.input(
|
||||
z.object({
|
||||
token: z.string(),
|
||||
newPassword: z.string().min(8),
|
||||
newPasswordConfirmation: z.string().min(8)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { token, newPassword } = input;
|
||||
const { token, newPassword, newPasswordConfirmation } = input;
|
||||
|
||||
if (newPassword !== newPasswordConfirmation) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Password Mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
// Verify JWT token
|
||||
@@ -718,7 +743,7 @@ export const authRouter = createTRPCRouter({
|
||||
|
||||
// Resend email verification
|
||||
resendEmailVerification: publicProcedure
|
||||
.input(z.object({ email: emailSchema }))
|
||||
.input(z.object({ email: z.string().email() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const { email } = input;
|
||||
|
||||
@@ -827,3 +852,4 @@ export const authRouter = createTRPCRouter({
|
||||
return { success: true };
|
||||
})
|
||||
});
|
||||
|
||||
|
||||
@@ -11,13 +11,6 @@ import {
|
||||
import { setCookie } from "vinxi/http";
|
||||
import type { User } from "~/types/user";
|
||||
import { toUserProfile } from "~/types/user";
|
||||
import {
|
||||
updateEmailSchema,
|
||||
updateDisplayNameSchema,
|
||||
passwordChangeSchema,
|
||||
passwordSetSchema,
|
||||
deleteAccountSchema
|
||||
} from "~/server/api/schemas/validation";
|
||||
|
||||
export const userRouter = createTRPCRouter({
|
||||
// Get current user profile
|
||||
@@ -50,7 +43,7 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
// Update email
|
||||
updateEmail: publicProcedure
|
||||
.input(updateEmailSchema)
|
||||
.input(z.object({ email: z.string().email() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = await getUserID(ctx.event.nativeEvent);
|
||||
|
||||
@@ -87,7 +80,7 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
// Update display name
|
||||
updateDisplayName: publicProcedure
|
||||
.input(updateDisplayNameSchema)
|
||||
.input(z.object({ displayName: z.string().min(1).max(50) }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = await getUserID(ctx.event.nativeEvent);
|
||||
|
||||
@@ -118,9 +111,7 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
// Update profile image
|
||||
updateProfileImage: publicProcedure
|
||||
.input(
|
||||
z.object({ imageUrl: z.string().url().optional().or(z.literal("")) })
|
||||
)
|
||||
.input(z.object({ imageUrl: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = await getUserID(ctx.event.nativeEvent);
|
||||
|
||||
@@ -151,7 +142,13 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
// Change password (requires old password)
|
||||
changePassword: publicProcedure
|
||||
.input(passwordChangeSchema)
|
||||
.input(
|
||||
z.object({
|
||||
oldPassword: z.string(),
|
||||
newPassword: z.string().min(8),
|
||||
newPasswordConfirmation: z.string().min(8)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = await getUserID(ctx.event.nativeEvent);
|
||||
|
||||
@@ -162,7 +159,14 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const { oldPassword, newPassword } = input;
|
||||
const { oldPassword, newPassword, newPasswordConfirmation } = input;
|
||||
|
||||
if (newPassword !== newPasswordConfirmation) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Password Mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
const conn = ConnectionFactory();
|
||||
const res = await conn.execute({
|
||||
@@ -220,7 +224,12 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
// Set password (for OAuth users who don't have password)
|
||||
setPassword: publicProcedure
|
||||
.input(passwordSetSchema)
|
||||
.input(
|
||||
z.object({
|
||||
newPassword: z.string().min(8),
|
||||
newPasswordConfirmation: z.string().min(8)
|
||||
})
|
||||
)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = await getUserID(ctx.event.nativeEvent);
|
||||
|
||||
@@ -231,7 +240,14 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
const { password } = input;
|
||||
const { newPassword, newPasswordConfirmation } = input;
|
||||
|
||||
if (newPassword !== newPasswordConfirmation) {
|
||||
throw new TRPCError({
|
||||
code: "BAD_REQUEST",
|
||||
message: "Password Mismatch"
|
||||
});
|
||||
}
|
||||
|
||||
const conn = ConnectionFactory();
|
||||
const res = await conn.execute({
|
||||
@@ -256,7 +272,7 @@ export const userRouter = createTRPCRouter({
|
||||
}
|
||||
|
||||
// Set password
|
||||
const passwordHash = await hashPassword(password);
|
||||
const passwordHash = await hashPassword(newPassword);
|
||||
await conn.execute({
|
||||
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
||||
args: [passwordHash, userId]
|
||||
@@ -277,7 +293,7 @@ export const userRouter = createTRPCRouter({
|
||||
|
||||
// Delete account (anonymize data)
|
||||
deleteAccount: publicProcedure
|
||||
.input(deleteAccountSchema)
|
||||
.input(z.object({ password: z.string() }))
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
const userId = await getUserID(ctx.event.nativeEvent);
|
||||
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import { z } from "zod";
|
||||
|
||||
/**
|
||||
* Validation Schemas for tRPC Procedures
|
||||
*
|
||||
* These schemas are the source of truth for server-side validation.
|
||||
* Client-side validation (src/lib/validation.ts) is optional for UX only.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Base Schemas
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Email validation schema
|
||||
* - Must be valid email format
|
||||
* - Min 3 chars, max 255 chars
|
||||
* - Trimmed and lowercased automatically
|
||||
*/
|
||||
export const emailSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.email("Invalid email address")
|
||||
.min(3, "Email too short")
|
||||
.max(255, "Email too long");
|
||||
|
||||
/**
|
||||
* Password validation schema
|
||||
* - Minimum 8 characters
|
||||
* - Maximum 128 characters
|
||||
* - Can add additional complexity requirements if needed
|
||||
*/
|
||||
export const passwordSchema = z
|
||||
.string()
|
||||
.min(8, "Password must be at least 8 characters")
|
||||
.max(128, "Password too long");
|
||||
|
||||
/**
|
||||
* Display name validation schema
|
||||
* - Minimum 1 character (after trim)
|
||||
* - Maximum 50 characters
|
||||
*/
|
||||
export const displayNameSchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Display name is required")
|
||||
.max(50, "Display name too long");
|
||||
|
||||
/**
|
||||
* Comment body validation schema
|
||||
* - Minimum 1 character (after trim)
|
||||
* - Maximum 10,000 characters
|
||||
*/
|
||||
export const commentBodySchema = z
|
||||
.string()
|
||||
.trim()
|
||||
.min(1, "Comment cannot be empty")
|
||||
.max(10000, "Comment too long (max 10,000 characters)");
|
||||
|
||||
// ============================================================================
|
||||
// Composed Schemas
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Email/password login schema
|
||||
*/
|
||||
export const loginSchema = z.object({
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
rememberMe: z.boolean().optional()
|
||||
});
|
||||
|
||||
/**
|
||||
* Email/password registration schema with password confirmation
|
||||
*/
|
||||
export const registrationSchema = z
|
||||
.object({
|
||||
email: emailSchema,
|
||||
password: passwordSchema,
|
||||
passwordConfirmation: passwordSchema,
|
||||
displayName: displayNameSchema.optional()
|
||||
})
|
||||
.refine((data) => data.password === data.passwordConfirmation, {
|
||||
message: "Passwords do not match",
|
||||
path: ["passwordConfirmation"]
|
||||
});
|
||||
|
||||
/**
|
||||
* Password change schema (requires old password)
|
||||
*/
|
||||
export const passwordChangeSchema = z
|
||||
.object({
|
||||
oldPassword: passwordSchema,
|
||||
newPassword: passwordSchema,
|
||||
newPasswordConfirmation: passwordSchema
|
||||
})
|
||||
.refine((data) => data.newPassword === data.newPasswordConfirmation, {
|
||||
message: "New passwords do not match",
|
||||
path: ["newPasswordConfirmation"]
|
||||
})
|
||||
.refine((data) => data.oldPassword !== data.newPassword, {
|
||||
message: "New password must be different from old password",
|
||||
path: ["newPassword"]
|
||||
});
|
||||
|
||||
/**
|
||||
* Password reset schema (no old password required)
|
||||
*/
|
||||
export const passwordResetSchema = z
|
||||
.object({
|
||||
token: z.string(),
|
||||
newPassword: passwordSchema,
|
||||
newPasswordConfirmation: passwordSchema
|
||||
})
|
||||
.refine((data) => data.newPassword === data.newPasswordConfirmation, {
|
||||
message: "Passwords do not match",
|
||||
path: ["newPasswordConfirmation"]
|
||||
});
|
||||
|
||||
/**
|
||||
* Password set schema (for OAuth users setting password first time)
|
||||
*/
|
||||
export const passwordSetSchema = z
|
||||
.object({
|
||||
password: passwordSchema,
|
||||
passwordConfirmation: passwordSchema
|
||||
})
|
||||
.refine((data) => data.password === data.passwordConfirmation, {
|
||||
message: "Passwords do not match",
|
||||
path: ["passwordConfirmation"]
|
||||
});
|
||||
|
||||
/**
|
||||
* Update email schema
|
||||
*/
|
||||
export const updateEmailSchema = z.object({
|
||||
email: emailSchema
|
||||
});
|
||||
|
||||
/**
|
||||
* Update display name schema
|
||||
*/
|
||||
export const updateDisplayNameSchema = z.object({
|
||||
displayName: displayNameSchema
|
||||
});
|
||||
|
||||
/**
|
||||
* Account deletion schema
|
||||
*/
|
||||
export const deleteAccountSchema = z.object({
|
||||
password: passwordSchema
|
||||
});
|
||||
Reference in New Issue
Block a user