Files
livedash-node/server/routers/auth.ts
Kaj Kowalski fa7e815a3b feat: complete tRPC integration and fix platform UI issues
- Implement comprehensive tRPC setup with type-safe API
- Create tRPC routers for dashboard, admin, and auth endpoints
- Migrate frontend components to use tRPC client
- Fix platform dashboard Settings button functionality
- Add platform settings page with profile and security management
- Create OpenAI API mocking infrastructure for cost-safe testing
- Update tests to work with new tRPC architecture
- Sync database schema to fix AIBatchRequest table errors
2025-07-12 00:27:57 +02:00

329 lines
7.5 KiB
TypeScript

/**
* Authentication tRPC Router
*
* Handles user authentication operations:
* - User registration
* - Login validation
* - Password reset requests
* - User profile management
*/
import {
router,
publicProcedure,
protectedProcedure,
rateLimitedProcedure,
} from "@/lib/trpc";
import { TRPCError } from "@trpc/server";
import {
registerSchema,
loginSchema,
forgotPasswordSchema,
userUpdateSchema,
} from "@/lib/validation";
import bcrypt from "bcryptjs";
import { z } from "zod";
export const authRouter = router({
/**
* Register a new user
*/
register: rateLimitedProcedure
.input(registerSchema)
.mutation(async ({ input, ctx }) => {
const { email, password, company: companyName } = input;
// Check if user already exists
const existingUser = await ctx.prisma.user.findUnique({
where: { email },
});
if (existingUser) {
throw new TRPCError({
code: "CONFLICT",
message: "User with this email already exists",
});
}
// Hash password
const hashedPassword = await bcrypt.hash(password, 12);
// Create or find company
let company = await ctx.prisma.company.findFirst({
where: {
name: {
equals: companyName,
mode: "insensitive",
},
},
});
if (!company) {
company = await ctx.prisma.company.create({
data: {
name: companyName,
status: "ACTIVE",
csvUrl: `https://placeholder-${companyName.toLowerCase().replace(/\s+/g, "-")}.example.com/api/sessions.csv`,
},
});
}
// Create user
const user = await ctx.prisma.user.create({
data: {
email,
password: hashedPassword,
companyId: company.id,
role: "ADMIN", // First user is admin
},
select: {
id: true,
email: true,
role: true,
company: {
select: {
id: true,
name: true,
},
},
},
});
return {
message: "User registered successfully",
user,
};
}),
/**
* Validate login credentials
*/
validateLogin: publicProcedure
.input(loginSchema)
.query(async ({ input, ctx }) => {
const { email, password } = input;
const user = await ctx.prisma.user.findUnique({
where: { email },
include: {
company: {
select: {
id: true,
name: true,
status: true,
},
},
},
});
if (!user || !(await bcrypt.compare(password, user.password))) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "Invalid email or password",
});
}
if (user.company?.status !== "ACTIVE") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Company account is not active",
});
}
return {
user: {
id: user.id,
email: user.email,
role: user.role,
company: user.company,
},
};
}),
/**
* Request password reset
*/
forgotPassword: rateLimitedProcedure
.input(forgotPasswordSchema)
.mutation(async ({ input, ctx }) => {
const { email } = input;
const user = await ctx.prisma.user.findUnique({
where: { email },
});
if (!user) {
// Don't reveal if email exists or not
return {
message:
"If an account with that email exists, you will receive a password reset link.",
};
}
// Generate reset token (in real implementation, this would be a secure token)
const resetToken = Math.random().toString(36).substring(2, 15);
const resetTokenExpiry = new Date(Date.now() + 3600000); // 1 hour
await ctx.prisma.user.update({
where: { id: user.id },
data: {
resetToken,
resetTokenExpiry,
},
});
// TODO: Send email with reset link
// For now, just log the token (remove in production)
console.log(`Password reset token for ${email}: ${resetToken}`);
return {
message:
"If an account with that email exists, you will receive a password reset link.",
};
}),
/**
* Get current user profile
*/
getProfile: protectedProcedure.query(async ({ ctx }) => {
const user = await ctx.prisma.user.findUnique({
where: { email: ctx.session.user.email! },
include: {
company: {
select: {
id: true,
name: true,
status: true,
},
},
},
});
if (!user) {
throw new TRPCError({
code: "NOT_FOUND",
message: "User not found",
});
}
return {
id: user.id,
email: user.email,
role: user.role,
createdAt: user.createdAt,
company: user.company,
};
}),
/**
* Update user profile
*/
updateProfile: protectedProcedure
.input(userUpdateSchema)
.mutation(async ({ input, ctx }) => {
const updateData: any = {};
if (input.email) {
// Check if new email is already taken
const existingUser = await ctx.prisma.user.findUnique({
where: { email: input.email },
});
if (existingUser && existingUser.email !== ctx.session.user.email) {
throw new TRPCError({
code: "CONFLICT",
message: "Email is already taken",
});
}
updateData.email = input.email;
}
if (input.password) {
updateData.password = await bcrypt.hash(input.password, 12);
}
if (input.role) {
// Only admins can change roles
const currentUser = await ctx.prisma.user.findUnique({
where: { email: ctx.session.user.email! },
});
if (currentUser?.role !== "ADMIN") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Only admins can change user roles",
});
}
updateData.role = input.role;
}
const updatedUser = await ctx.prisma.user.update({
where: { email: ctx.session.user.email! },
data: updateData,
select: {
id: true,
email: true,
role: true,
company: {
select: {
id: true,
name: true,
},
},
},
});
return {
message: "Profile updated successfully",
user: updatedUser,
};
}),
/**
* Reset password with token
*/
resetPassword: publicProcedure
.input(
z.object({
token: z.string().min(1, "Reset token is required"),
password: registerSchema.shape.password,
})
)
.mutation(async ({ input, ctx }) => {
const { token, password } = input;
const user = await ctx.prisma.user.findFirst({
where: {
resetToken: token,
resetTokenExpiry: {
gt: new Date(),
},
},
});
if (!user) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Invalid or expired reset token",
});
}
const hashedPassword = await bcrypt.hash(password, 12);
await ctx.prisma.user.update({
where: { id: user.id },
data: {
password: hashedPassword,
resetToken: null,
resetTokenExpiry: null,
},
});
return {
message: "Password reset successfully",
};
}),
});