Skip to content

Commit 17ea0df

Browse files
feat(frontend): Add confirm password and fix sign up problem when user does not verify. (#188)
![image](https://github.com/user-attachments/assets/3b536974-425a-4f43-8c93-a5c08800ca73) ![image](https://github.com/user-attachments/assets/3f40b41f-96df-4b37-b4cd-05c26348b046) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **New Features** - Enhanced registration with a password confirmation step, ensuring passwords match and meet minimum length requirements. - Real-time password strength feedback and improved error messaging during sign-up. - An additional sign-up option via GitHub. - Smoothed registration flow with updates to handle unconfirmed emails more effectively. - Improved sign-up modal layout for clearer user instructions and better responsiveness. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
1 parent b8d9665 commit 17ea0df

File tree

4 files changed

+206
-9
lines changed

4 files changed

+206
-9
lines changed

backend/src/auth/auth.service.ts

+15-3
Original file line numberDiff line numberDiff line change
@@ -142,19 +142,31 @@ export class AuthService {
142142
}
143143

144144
async register(registerUserInput: RegisterUserInput): Promise<User> {
145-
const { username, email, password } = registerUserInput;
145+
const { username, email, password, confirmPassword } = registerUserInput;
146146

147147
// Check for existing email
148148
const existingUser = await this.userRepository.findOne({
149149
where: { email },
150150
});
151151

152-
if (existingUser) {
153-
throw new ConflictException('Email already exists');
152+
if (password !== confirmPassword) {
153+
throw new ConflictException('Passwords do not match');
154154
}
155155

156156
const hashedPassword = await hash(password, 10);
157157

158+
// If the user exists but email is not confirmed and mail is enabled
159+
if (existingUser && !existingUser.isEmailConfirmed && this.isMailEnabled) {
160+
// Just update the existing user and resend verification email
161+
existingUser.username = username;
162+
existingUser.password = hashedPassword;
163+
await this.userRepository.save(existingUser);
164+
await this.sendVerificationEmail(existingUser);
165+
return existingUser;
166+
} else if (existingUser) {
167+
throw new ConflictException('Email already exists');
168+
}
169+
158170
let newUser;
159171
if (this.isMailEnabled) {
160172
newUser = this.userRepository.create({

backend/src/user/dto/register-user.input.ts

+5
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,11 @@ export class RegisterUserInput {
1313
@MinLength(6)
1414
password: string;
1515

16+
@Field()
17+
@IsString()
18+
@MinLength(6)
19+
confirmPassword: string;
20+
1621
@Field()
1722
@IsEmail()
1823
email: string;

backend/src/user/user.resolver.ts

+4
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,10 @@ export class UserResolver {
6363
async registerUser(
6464
@Args('input') registerUserInput: RegisterUserInput,
6565
): Promise<User> {
66+
if (registerUserInput.password.length < 6) {
67+
throw new Error('Password must be at least 6 characters');
68+
}
69+
6670
return this.authService.register(registerUserInput);
6771
}
6872

frontend/src/components/sign-up-modal.tsx

+182-6
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ import {
2424
} from '@/graphql/mutations/auth';
2525
import { useRouter } from 'next/navigation';
2626
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
27-
import { AlertCircle, CheckCircle, Mail, Clock } from 'lucide-react';
27+
import { AlertCircle, CheckCircle, Mail, Clock, Github } from 'lucide-react';
2828
import { useEffect } from 'react';
2929
import { logger } from '@/app/log/logger';
3030

@@ -43,6 +43,48 @@ export function SignUpModal({
4343
const [registrationSuccess, setRegistrationSuccess] = useState(false);
4444
const [resendCooldown, setResendCooldown] = useState(0);
4545
const [resendMessage, setResendMessage] = useState<string | null>(null);
46+
const [passwordConfirm, setPasswordConfirm] = useState('');
47+
const [passwordError, setPasswordError] = useState<string | null>(null);
48+
const [passwordStrength, setPasswordStrength] = useState<
49+
'weak' | 'medium' | 'strong' | null
50+
>(null);
51+
52+
const validatePassword = (value: string) => {
53+
// Reset errors
54+
setPasswordError(null);
55+
56+
// Check minimum length
57+
if (value.length < 6) {
58+
setPasswordError('Password must be at least 6 characters long');
59+
setPasswordStrength('weak');
60+
return false;
61+
}
62+
63+
// Check for complexity
64+
const hasUppercase = /[A-Z]/.test(value);
65+
const hasLowercase = /[a-z]/.test(value);
66+
const hasNumbers = /\d/.test(value);
67+
const hasSpecialChar = /[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(value);
68+
69+
const strengthScore = [
70+
hasUppercase,
71+
hasLowercase,
72+
hasNumbers,
73+
hasSpecialChar,
74+
].filter(Boolean).length;
75+
76+
if (strengthScore < 2) {
77+
setPasswordStrength('weak');
78+
setPasswordError('Password is too weak');
79+
return false;
80+
} else if (strengthScore < 4) {
81+
setPasswordStrength('medium');
82+
return true;
83+
} else {
84+
setPasswordStrength('strong');
85+
return true;
86+
}
87+
};
4688

4789
const [registerUser, { loading }] = useMutation(REGISTER_USER, {
4890
onError: (error) => {
@@ -61,18 +103,28 @@ export function SignUpModal({
61103
e.preventDefault();
62104
setErrorMessage(null);
63105

64-
if (!name || !email || !password) {
106+
if (!name || !email || !password || !passwordConfirm) {
65107
setErrorMessage('All fields are required.');
66108
return;
67109
}
68110

111+
if (!validatePassword(password)) {
112+
return;
113+
}
114+
115+
if (password !== passwordConfirm) {
116+
setErrorMessage('Passwords do not match.');
117+
return;
118+
}
119+
69120
try {
70121
await registerUser({
71122
variables: {
72123
input: {
73124
username: name,
74125
email,
75126
password,
127+
confirmPassword: passwordConfirm,
76128
},
77129
},
78130
});
@@ -133,15 +185,15 @@ export function SignUpModal({
133185

134186
return (
135187
<Dialog open={isOpen} onOpenChange={onClose}>
136-
<DialogContent className="sm:max-w-[425px] fixed top-[50%] left-[50%] transform -translate-x-[50%] -translate-y-[50%] p-0">
188+
<DialogContent className="sm:max-w-[425px] fixed top-[50%] left-[50%] transform -translate-x-[50%] -translate-y-[50%] p-0 max-h-[90vh] overflow-y-auto">
137189
<VisuallyHidden>
138190
<DialogTitle>Sign Up</DialogTitle>
139191
<DialogDescription>
140192
Create an account by entering your information below
141193
</DialogDescription>
142194
</VisuallyHidden>
143195

144-
<BackgroundGradient className="rounded-[22px] p-4 bg-white dark:bg-zinc-900">
196+
<BackgroundGradient className="rounded-[22px] p-4 bg-white dark:bg-zinc-900 overflow-hidden">
145197
<div className="w-full">
146198
{registrationSuccess ? (
147199
<>
@@ -213,13 +265,50 @@ export function SignUpModal({
213265
) : (
214266
<>
215267
<TextureCardHeader className="flex flex-col gap-1 items-center justify-center p-4">
216-
<TextureCardTitle>Create your account</TextureCardTitle>
268+
<TextureCardTitle>Create account</TextureCardTitle>
217269
<p className="text-center text-neutral-600 dark:text-neutral-400">
218-
Welcome! Please fill in the details to get started.
270+
Enter your information to create your account
219271
</p>
220272
</TextureCardHeader>
221273
<TextureSeparator />
222274
<TextureCardContent>
275+
<Button
276+
variant="outline"
277+
className="flex items-center justify-center gap-2 w-full"
278+
type="button"
279+
>
280+
<img
281+
src="/images/google.svg"
282+
alt="Google"
283+
className="w-5 h-5"
284+
/>
285+
<span>Continue with Google</span>
286+
</Button>
287+
288+
{/* GitHub Sign Up Button - added below Google */}
289+
<div className="mt-4">
290+
<Button
291+
variant="outline"
292+
className="flex items-center justify-center gap-2 w-full"
293+
type="button"
294+
>
295+
<Github className="w-5 h-5 text-black dark:text-white" />
296+
<span>Continue with GitHub</span>
297+
</Button>
298+
</div>
299+
300+
{/* Divider with "or" text */}
301+
<div className="relative my-6">
302+
<div className="absolute inset-0 flex items-center">
303+
<div className="w-full border-t border-gray-300 dark:border-gray-700"></div>
304+
</div>
305+
<div className="relative flex justify-center text-sm">
306+
<span className="px-2 bg-white dark:bg-zinc-900 text-gray-500">
307+
Or continue with
308+
</span>
309+
</div>
310+
</div>
311+
223312
<form onSubmit={handleSubmit} className="space-y-2">
224313
<div className="space-y-1">
225314
<Label htmlFor="name">Name</Label>
@@ -260,6 +349,92 @@ export function SignUpModal({
260349
value={password}
261350
onChange={(e) => {
262351
setPassword(e.target.value);
352+
validatePassword(e.target.value);
353+
setErrorMessage(null);
354+
}}
355+
required
356+
className={`w-full ${passwordError ? 'border-red-500' : ''}`}
357+
/>
358+
{password && (
359+
<div className="mt-2 space-y-2">
360+
<div className="flex items-center gap-2">
361+
<div className="text-sm">Password strength:</div>
362+
<div className="flex h-2 w-full max-w-[100px] overflow-hidden rounded-full bg-gray-200 dark:bg-gray-700">
363+
<div
364+
className={`h-full ${
365+
passwordStrength === 'weak'
366+
? 'w-1/3 bg-red-500'
367+
: passwordStrength === 'medium'
368+
? 'w-2/3 bg-yellow-500'
369+
: 'w-full bg-green-500'
370+
}`}
371+
/>
372+
</div>
373+
<div className="text-sm">
374+
{passwordStrength === 'weak'
375+
? 'Weak'
376+
: passwordStrength === 'medium'
377+
? 'Medium'
378+
: 'Strong'}
379+
</div>
380+
</div>
381+
382+
<div className="text-xs text-gray-500 dark:text-gray-400">
383+
Password must:
384+
<ul className="list-disc pl-5 mt-1 space-y-1">
385+
<li
386+
className={
387+
password.length >= 6 ? 'text-green-500' : ''
388+
}
389+
>
390+
Be at least 6 characters long
391+
</li>
392+
<li
393+
className={
394+
/[A-Z]/.test(password) ? 'text-green-500' : ''
395+
}
396+
>
397+
Include at least one uppercase letter
398+
</li>
399+
<li
400+
className={
401+
/\d/.test(password) ? 'text-green-500' : ''
402+
}
403+
>
404+
Include at least one number
405+
</li>
406+
<li
407+
className={
408+
/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/.test(
409+
password
410+
)
411+
? 'text-green-500'
412+
: ''
413+
}
414+
>
415+
Include at least one special character
416+
</li>
417+
</ul>
418+
</div>
419+
</div>
420+
)}
421+
422+
{passwordError && (
423+
<div className="text-red-500 text-xs mt-1">
424+
{passwordError}
425+
</div>
426+
)}
427+
</div>
428+
429+
<div className="space-y-1">
430+
<Label htmlFor="passwordConfirm">Confirm Password</Label>
431+
<Input
432+
id="passwordConfirm"
433+
placeholder="Confirm Password"
434+
type="password"
435+
value={passwordConfirm}
436+
onChange={(e) => {
437+
setPasswordConfirm(e.target.value);
263438
setErrorMessage(null);
264439
}}
265440
required
@@ -274,6 +449,7 @@ export function SignUpModal({
274449
</div>
275450
)}
276451

452+
<div className="h-2"></div>
277453
<Button type="submit" className="w-full" disabled={loading}>
278454
{loading ? 'Signing up...' : 'Sign up'}
279455
</Button>

0 commit comments

Comments
 (0)