366 lines
9.1 KiB
TypeScript
366 lines
9.1 KiB
TypeScript
import { createTRPCRouter, publicProcedure } from "../utils";
|
|
import { z } from "zod";
|
|
import { TRPCError } from "@trpc/server";
|
|
import { env } from "~/env/server";
|
|
import {
|
|
ConnectionFactory,
|
|
getUserID,
|
|
hashPassword,
|
|
checkPassword
|
|
} from "~/server/utils";
|
|
import { setCookie } from "vinxi/http";
|
|
import type { User } from "~/types/user";
|
|
import { toUserProfile } from "~/types/user";
|
|
|
|
export const userRouter = createTRPCRouter({
|
|
// Get current user profile
|
|
getProfile: publicProcedure.query(async ({ ctx }) => {
|
|
const userId = await getUserID(ctx.event.nativeEvent);
|
|
|
|
if (!userId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Not authenticated"
|
|
});
|
|
}
|
|
|
|
const conn = ConnectionFactory();
|
|
const res = await conn.execute({
|
|
sql: "SELECT * FROM User WHERE id = ?",
|
|
args: [userId]
|
|
});
|
|
|
|
if (res.rows.length === 0) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "User not found"
|
|
});
|
|
}
|
|
|
|
const user = res.rows[0] as unknown as User;
|
|
return toUserProfile(user);
|
|
}),
|
|
|
|
// Update email
|
|
updateEmail: publicProcedure
|
|
.input(z.object({ email: z.string().email() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const userId = await getUserID(ctx.event.nativeEvent);
|
|
|
|
if (!userId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Not authenticated"
|
|
});
|
|
}
|
|
|
|
const { email } = input;
|
|
const conn = ConnectionFactory();
|
|
|
|
await conn.execute({
|
|
sql: "UPDATE User SET email = ?, email_verified = ? WHERE id = ?",
|
|
args: [email, 0, userId]
|
|
});
|
|
|
|
// Fetch updated user
|
|
const res = await conn.execute({
|
|
sql: "SELECT * FROM User WHERE id = ?",
|
|
args: [userId]
|
|
});
|
|
|
|
const user = res.rows[0] as unknown as User;
|
|
|
|
// Set email cookie for verification flow
|
|
setCookie(ctx.event.nativeEvent, "emailToken", email, {
|
|
path: "/"
|
|
});
|
|
|
|
return toUserProfile(user);
|
|
}),
|
|
|
|
// Update display name
|
|
updateDisplayName: publicProcedure
|
|
.input(z.object({ displayName: z.string().min(1).max(50) }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const userId = await getUserID(ctx.event.nativeEvent);
|
|
|
|
if (!userId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Not authenticated"
|
|
});
|
|
}
|
|
|
|
const { displayName } = input;
|
|
const conn = ConnectionFactory();
|
|
|
|
await conn.execute({
|
|
sql: "UPDATE User SET display_name = ? WHERE id = ?",
|
|
args: [displayName, userId]
|
|
});
|
|
|
|
// Fetch updated user
|
|
const res = await conn.execute({
|
|
sql: "SELECT * FROM User WHERE id = ?",
|
|
args: [userId]
|
|
});
|
|
|
|
const user = res.rows[0] as unknown as User;
|
|
return toUserProfile(user);
|
|
}),
|
|
|
|
// Update profile image
|
|
updateProfileImage: publicProcedure
|
|
.input(z.object({ imageUrl: z.string() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const userId = await getUserID(ctx.event.nativeEvent);
|
|
|
|
if (!userId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Not authenticated"
|
|
});
|
|
}
|
|
|
|
const { imageUrl } = input;
|
|
const conn = ConnectionFactory();
|
|
|
|
await conn.execute({
|
|
sql: "UPDATE User SET image = ? WHERE id = ?",
|
|
args: [imageUrl, userId]
|
|
});
|
|
|
|
// Fetch updated user
|
|
const res = await conn.execute({
|
|
sql: "SELECT * FROM User WHERE id = ?",
|
|
args: [userId]
|
|
});
|
|
|
|
const user = res.rows[0] as unknown as User;
|
|
return toUserProfile(user);
|
|
}),
|
|
|
|
// Change password (requires old password)
|
|
changePassword: publicProcedure
|
|
.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);
|
|
|
|
if (!userId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Not authenticated"
|
|
});
|
|
}
|
|
|
|
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({
|
|
sql: "SELECT * FROM User WHERE id = ?",
|
|
args: [userId]
|
|
});
|
|
|
|
if (res.rows.length === 0) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "User not found"
|
|
});
|
|
}
|
|
|
|
const user = res.rows[0] as unknown as User;
|
|
|
|
if (!user.password_hash) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "No password set"
|
|
});
|
|
}
|
|
|
|
const passwordMatch = await checkPassword(
|
|
oldPassword,
|
|
user.password_hash
|
|
);
|
|
|
|
if (!passwordMatch) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Password did not match record"
|
|
});
|
|
}
|
|
|
|
// Update password
|
|
const newPasswordHash = await hashPassword(newPassword);
|
|
await conn.execute({
|
|
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
|
args: [newPasswordHash, userId]
|
|
});
|
|
|
|
// Clear session cookies (force re-login)
|
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
|
maxAge: 0,
|
|
path: "/"
|
|
});
|
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
|
maxAge: 0,
|
|
path: "/"
|
|
});
|
|
|
|
return { success: true, message: "success" };
|
|
}),
|
|
|
|
// Set password (for OAuth users who don't have password)
|
|
setPassword: publicProcedure
|
|
.input(
|
|
z.object({
|
|
newPassword: z.string().min(8),
|
|
newPasswordConfirmation: z.string().min(8)
|
|
})
|
|
)
|
|
.mutation(async ({ input, ctx }) => {
|
|
const userId = await getUserID(ctx.event.nativeEvent);
|
|
|
|
if (!userId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Not authenticated"
|
|
});
|
|
}
|
|
|
|
const { newPassword, newPasswordConfirmation } = input;
|
|
|
|
if (newPassword !== newPasswordConfirmation) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Password Mismatch"
|
|
});
|
|
}
|
|
|
|
const conn = ConnectionFactory();
|
|
const res = await conn.execute({
|
|
sql: "SELECT * FROM User WHERE id = ?",
|
|
args: [userId]
|
|
});
|
|
|
|
if (res.rows.length === 0) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "User not found"
|
|
});
|
|
}
|
|
|
|
const user = res.rows[0] as unknown as User;
|
|
|
|
if (user.password_hash) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Password exists"
|
|
});
|
|
}
|
|
|
|
// Set password
|
|
const passwordHash = await hashPassword(newPassword);
|
|
await conn.execute({
|
|
sql: "UPDATE User SET password_hash = ? WHERE id = ?",
|
|
args: [passwordHash, userId]
|
|
});
|
|
|
|
// Clear session cookies (force re-login)
|
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
|
maxAge: 0,
|
|
path: "/"
|
|
});
|
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
|
maxAge: 0,
|
|
path: "/"
|
|
});
|
|
|
|
return { success: true, message: "success" };
|
|
}),
|
|
|
|
// Delete account (anonymize data)
|
|
deleteAccount: publicProcedure
|
|
.input(z.object({ password: z.string() }))
|
|
.mutation(async ({ input, ctx }) => {
|
|
const userId = await getUserID(ctx.event.nativeEvent);
|
|
|
|
if (!userId) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Not authenticated"
|
|
});
|
|
}
|
|
|
|
const { password } = input;
|
|
const conn = ConnectionFactory();
|
|
|
|
const res = await conn.execute({
|
|
sql: "SELECT * FROM User WHERE id = ?",
|
|
args: [userId]
|
|
});
|
|
|
|
if (res.rows.length === 0) {
|
|
throw new TRPCError({
|
|
code: "NOT_FOUND",
|
|
message: "User not found"
|
|
});
|
|
}
|
|
|
|
const user = res.rows[0] as unknown as User;
|
|
|
|
if (!user.password_hash) {
|
|
throw new TRPCError({
|
|
code: "BAD_REQUEST",
|
|
message: "Password required"
|
|
});
|
|
}
|
|
|
|
const passwordMatch = await checkPassword(password, user.password_hash);
|
|
|
|
if (!passwordMatch) {
|
|
throw new TRPCError({
|
|
code: "UNAUTHORIZED",
|
|
message: "Password Did Not Match"
|
|
});
|
|
}
|
|
|
|
// Anonymize user data (don't hard delete)
|
|
await conn.execute({
|
|
sql: `UPDATE User SET
|
|
email = ?,
|
|
email_verified = ?,
|
|
password_hash = ?,
|
|
display_name = ?,
|
|
provider = ?,
|
|
image = ?
|
|
WHERE id = ?`,
|
|
args: [null, 0, null, "user deleted", null, null, userId]
|
|
});
|
|
|
|
// Clear session cookies
|
|
setCookie(ctx.event.nativeEvent, "emailToken", "", {
|
|
maxAge: 0,
|
|
path: "/"
|
|
});
|
|
setCookie(ctx.event.nativeEvent, "userIDToken", "", {
|
|
maxAge: 0,
|
|
path: "/"
|
|
});
|
|
|
|
return { success: true, message: "deleted" };
|
|
})
|
|
});
|