fix invite issue.

This commit is contained in:
Fizer Khan
2025-12-05 09:55:21 +05:30
parent 67976fc35b
commit 330fbcbd42
6 changed files with 283 additions and 58 deletions
+16
View File
@@ -6,6 +6,8 @@ import {
ForgotPasswordSchema,
ResetPasswordSchema,
MfaVerifySchema,
UpdateProfileSchema,
ChangePasswordSchema,
} from '../../../../shared/src/index';
import * as authService from './auth.service';
import { authenticate } from '../../common/middleware/auth';
@@ -105,4 +107,18 @@ export const authRoutes: FastifyPluginAsync = async (fastify) => {
await authService.disableMfa(request.user!.sub);
return { success: true };
});
// PATCH /auth/profile - Update profile
fastify.patch('/profile', { preHandler: authenticate }, async (request) => {
const body = UpdateProfileSchema.parse(request.body);
const user = await authService.updateProfile(request.user!.sub, body);
return { user };
});
// POST /auth/change-password - Change password
fastify.post('/change-password', { preHandler: authenticate }, async (request) => {
const body = ChangePasswordSchema.parse(request.body);
await authService.changePassword(request.user!.sub, body.currentPassword, body.newPassword);
return { message: 'Password changed successfully' };
});
};
+56
View File
@@ -352,6 +352,62 @@ export async function disableMfa(userId: string): Promise<void> {
});
}
// ==================== Profile Updates ====================
export async function updateProfile(
userId: string,
data: { firstName?: string; lastName?: string; email?: string }
): Promise<User> {
// If email is being changed, check for conflicts
if (data.email) {
const existingUser = await prisma.user.findFirst({
where: {
email: data.email.toLowerCase(),
id: { not: userId },
},
});
if (existingUser) {
throw new ConflictError('Email is already in use');
}
}
const user = await prisma.user.update({
where: { id: userId },
data: {
...(data.firstName && { firstName: data.firstName }),
...(data.lastName && { lastName: data.lastName }),
...(data.email && { email: data.email.toLowerCase() }),
},
});
return mapUser(user);
}
export async function changePassword(
userId: string,
currentPassword: string,
newPassword: string
): Promise<void> {
const user = await prisma.user.findUnique({
where: { id: userId },
});
if (!user) {
throw new NotFoundError('User');
}
const isValidPassword = await verifyPassword(user.passwordHash, currentPassword);
if (!isValidPassword) {
throw new ValidationError('Current password is incorrect');
}
const passwordHash = await hashPassword(newPassword);
await prisma.user.update({
where: { id: userId },
data: { passwordHash },
});
}
// ==================== Helpers ====================
async function generateTokens(
@@ -3,7 +3,7 @@ import { z } from 'zod';
import { randomBytes } from 'crypto';
import { prisma } from '../../lib/prisma';
import { authenticate, optionalAuthenticate } from '../../common/middleware/auth';
import { NotFoundError, ConflictError, ForbiddenError } from '@nats-console/shared';
import { NotFoundError, ConflictError, ForbiddenError } from '../../../../shared/src/index';
const CreateInviteSchema = z.object({
email: z.string().email(),
+17
View File
@@ -52,6 +52,21 @@ export const MfaVerifySchema = z.object({
code: z.string().length(6, 'MFA code must be 6 digits'),
});
export const UpdateProfileSchema = z.object({
firstName: z.string().min(1).max(100).optional(),
lastName: z.string().min(1).max(100).optional(),
email: z.string().email('Invalid email address').optional(),
});
export const ChangePasswordSchema = z.object({
currentPassword: z.string().min(1, 'Current password is required'),
newPassword: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Password must contain at least one uppercase letter')
.regex(/[a-z]/, 'Password must contain at least one lowercase letter')
.regex(/[0-9]/, 'Password must contain at least one number'),
});
// ==================== User Schemas ====================
export const UpdateUserSchema = z.object({
@@ -343,6 +358,8 @@ export type RefreshTokenInput = z.infer<typeof RefreshTokenSchema>;
export type ForgotPasswordInput = z.infer<typeof ForgotPasswordSchema>;
export type ResetPasswordInput = z.infer<typeof ResetPasswordSchema>;
export type MfaVerifyInput = z.infer<typeof MfaVerifySchema>;
export type UpdateProfileInput = z.infer<typeof UpdateProfileSchema>;
export type ChangePasswordInput = z.infer<typeof ChangePasswordSchema>;
export type UpdateUserInput = z.infer<typeof UpdateUserSchema>;
export type InviteUserInput = z.infer<typeof InviteUserSchema>;
export type CreateOrganizationInput = z.infer<typeof CreateOrganizationSchema>;
+181 -57
View File
@@ -1,8 +1,8 @@
'use client';
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { Save, User, Bell, Shield, Key, Palette, Users, UserPlus, Trash2, Mail } from 'lucide-react';
import { Save, User, Bell, Shield, Key, Palette, Users, UserPlus, Trash2, Mail, Loader2, CheckCircle2 } from 'lucide-react';
import { useAuthStore } from '@/stores/auth';
import { api } from '@/lib/api';
import { Button } from '@/components/ui/button';
@@ -11,16 +11,25 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/com
import { InviteUserDialog } from '@/components/forms/invite-user-dialog';
export default function SettingsPage() {
const { user } = useAuthStore();
const { user, setUser } = useAuthStore();
const [activeTab, setActiveTab] = useState('profile');
const queryClient = useQueryClient();
const [profileForm, setProfileForm] = useState({
firstName: user?.firstName || '',
lastName: user?.lastName || '',
email: user?.email || '',
firstName: '',
lastName: '',
email: '',
});
const [passwordForm, setPasswordForm] = useState({
currentPassword: '',
newPassword: '',
confirmPassword: '',
});
const [passwordError, setPasswordError] = useState('');
const [passwordSuccess, setPasswordSuccess] = useState(false);
const [notificationSettings, setNotificationSettings] = useState({
emailAlerts: true,
webhookAlerts: false,
@@ -30,18 +39,49 @@ export default function SettingsPage() {
const [showInviteDialog, setShowInviteDialog] = useState(false);
// Fetch team members
const { data: membersData } = useQuery({
queryKey: ['organization-members'],
queryFn: () => api.auth.me().then(() => []), // TODO: Add members endpoint
});
// Initialize form with user data
useEffect(() => {
if (user) {
setProfileForm({
firstName: user.firstName || '',
lastName: user.lastName || '',
email: user.email || '',
});
}
}, [user]);
// Fetch pending invites
const { data: invitesData, refetch: refetchInvites } = useQuery({
const { data: invitesData } = useQuery({
queryKey: ['invites'],
queryFn: () => api.invites.list(),
});
// Profile update mutation
const profileMutation = useMutation({
mutationFn: (data: { firstName?: string; lastName?: string; email?: string }) =>
api.auth.updateProfile(data),
onSuccess: (data) => {
setUser(data.user);
queryClient.invalidateQueries({ queryKey: ['user'] });
},
});
// Change password mutation
const changePasswordMutation = useMutation({
mutationFn: (data: { currentPassword: string; newPassword: string }) =>
api.auth.changePassword(data),
onSuccess: () => {
setPasswordForm({ currentPassword: '', newPassword: '', confirmPassword: '' });
setPasswordError('');
setPasswordSuccess(true);
setTimeout(() => setPasswordSuccess(false), 3000);
},
onError: (err: any) => {
setPasswordError(err.message || 'Failed to change password');
setPasswordSuccess(false);
},
});
const revokeMutation = useMutation({
mutationFn: (id: string) => api.invites.revoke(id),
onSuccess: () => {
@@ -49,6 +89,31 @@ export default function SettingsPage() {
},
});
const handleProfileSubmit = (e: React.FormEvent) => {
e.preventDefault();
profileMutation.mutate(profileForm);
};
const handlePasswordSubmit = (e: React.FormEvent) => {
e.preventDefault();
setPasswordError('');
if (passwordForm.newPassword !== passwordForm.confirmPassword) {
setPasswordError('Passwords do not match');
return;
}
if (passwordForm.newPassword.length < 8) {
setPasswordError('Password must be at least 8 characters');
return;
}
changePasswordMutation.mutate({
currentPassword: passwordForm.currentPassword,
newPassword: passwordForm.newPassword,
});
};
const tabs = [
{ id: 'profile', label: 'Profile', icon: User },
{ id: 'team', label: 'Team', icon: Users },
@@ -95,42 +160,59 @@ export default function SettingsPage() {
<CardTitle>Profile Settings</CardTitle>
<CardDescription>Update your personal information</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="grid grid-cols-2 gap-4">
<form onSubmit={handleProfileSubmit}>
<CardContent className="space-y-4">
{profileMutation.isSuccess && (
<div className="p-3 text-sm text-green-600 bg-green-50 rounded-md flex items-center gap-2">
<CheckCircle2 className="h-4 w-4" />
Profile updated successfully
</div>
)}
{profileMutation.error && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{(profileMutation.error as any).message || 'Failed to update profile'}
</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<label className="text-sm font-medium">First Name</label>
<Input
value={profileForm.firstName}
onChange={(e) =>
setProfileForm({ ...profileForm, firstName: e.target.value })
}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Last Name</label>
<Input
value={profileForm.lastName}
onChange={(e) =>
setProfileForm({ ...profileForm, lastName: e.target.value })
}
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">First Name</label>
<label className="text-sm font-medium">Email</label>
<Input
value={profileForm.firstName}
type="email"
value={profileForm.email}
onChange={(e) =>
setProfileForm({ ...profileForm, firstName: e.target.value })
setProfileForm({ ...profileForm, email: e.target.value })
}
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Last Name</label>
<Input
value={profileForm.lastName}
onChange={(e) =>
setProfileForm({ ...profileForm, lastName: e.target.value })
}
/>
</div>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Email</label>
<Input
type="email"
value={profileForm.email}
onChange={(e) =>
setProfileForm({ ...profileForm, email: e.target.value })
}
/>
</div>
<Button>
<Save className="h-4 w-4" />
Save Changes
</Button>
</CardContent>
<Button type="submit" disabled={profileMutation.isPending}>
{profileMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Save className="h-4 w-4" />
)}
Save Changes
</Button>
</CardContent>
</form>
</Card>
)}
@@ -328,21 +410,63 @@ export default function SettingsPage() {
<CardTitle>Change Password</CardTitle>
<CardDescription>Update your password regularly for security</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<label className="text-sm font-medium">Current Password</label>
<Input type="password" placeholder="Enter current password" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">New Password</label>
<Input type="password" placeholder="Enter new password" />
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Confirm New Password</label>
<Input type="password" placeholder="Confirm new password" />
</div>
<Button>Update Password</Button>
</CardContent>
<form onSubmit={handlePasswordSubmit}>
<CardContent className="space-y-4">
{passwordSuccess && (
<div className="p-3 text-sm text-green-600 bg-green-50 rounded-md flex items-center gap-2">
<CheckCircle2 className="h-4 w-4" />
Password changed successfully
</div>
)}
{passwordError && (
<div className="p-3 text-sm text-destructive bg-destructive/10 rounded-md">
{passwordError}
</div>
)}
<div className="space-y-2">
<label className="text-sm font-medium">Current Password</label>
<Input
type="password"
placeholder="Enter current password"
value={passwordForm.currentPassword}
onChange={(e) =>
setPasswordForm({ ...passwordForm, currentPassword: e.target.value })
}
required
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">New Password</label>
<Input
type="password"
placeholder="Enter new password"
value={passwordForm.newPassword}
onChange={(e) =>
setPasswordForm({ ...passwordForm, newPassword: e.target.value })
}
required
/>
</div>
<div className="space-y-2">
<label className="text-sm font-medium">Confirm New Password</label>
<Input
type="password"
placeholder="Confirm new password"
value={passwordForm.confirmPassword}
onChange={(e) =>
setPasswordForm({ ...passwordForm, confirmPassword: e.target.value })
}
required
/>
</div>
<Button type="submit" disabled={changePasswordMutation.isPending}>
{changePasswordMutation.isPending ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : null}
Update Password
</Button>
</CardContent>
</form>
</Card>
<Card>
+12
View File
@@ -113,6 +113,18 @@ export const auth = {
}),
me: () => request<{ user: any }>('/auth/me'),
updateProfile: (data: { firstName?: string; lastName?: string; email?: string }) =>
request<{ user: any }>('/auth/profile', {
method: 'PATCH',
body: data,
}),
changePassword: (data: { currentPassword: string; newPassword: string }) =>
request<{ message: string }>('/auth/change-password', {
method: 'POST',
body: data,
}),
};
// Clusters API