mirror of
https://github.com/kjanat/livedash-node.git
synced 2026-01-16 12:52:09 +01:00
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
This commit is contained in:
328
server/routers/auth.ts
Normal file
328
server/routers/auth.ts
Normal file
@ -0,0 +1,328 @@
|
||||
/**
|
||||
* 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",
|
||||
};
|
||||
}),
|
||||
});
|
||||
Reference in New Issue
Block a user