Skip to content

Commit 50df249

Browse files
committed
feat: enhance signup validation and error handling for inactive accounts and duplicates
1 parent 5975e24 commit 50df249

1 file changed

Lines changed: 85 additions & 18 deletions

File tree

src/routes/signup.ts

Lines changed: 85 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -8,49 +8,104 @@ import { sendWelcomeEmail } from '../utils/notifications'
88
const signupSchema = z.object({
99
email: z.string().email('Invalid email address'),
1010
password: z.string().min(8, 'Password must be at least 8 characters'),
11-
firstName: z.string().optional(),
12-
lastName: z.string().optional(),
11+
firstName: z.string().min(1, 'First name is required'),
12+
lastName: z.string().min(1, 'Last name is required'),
13+
whatsappNumber: z.string().optional(),
1314
})
1415

1516
export const signup = asyncHandler(async (req: Request, res: Response) => {
1617
let session
1718

1819
try {
19-
const { email, password, firstName, lastName } = signupSchema.parse(
20-
req.body,
21-
)
20+
const { email, password, firstName, lastName, whatsappNumber } =
21+
signupSchema.parse(req.body)
2222

2323
session = getSession()
2424

25-
// Check if user already exists (check both Member and User labels)
26-
const existingUser = await session.run(
27-
`MATCH (u:User {email: $email}) RETURN u LIMIT 1`,
25+
// 1. Check for inactive member first (they can be reactivated)
26+
const inactiveMemberResult = await session.run(
27+
`MATCH (m:Member {email: $email})
28+
WHERE m.markedInactive = true
29+
RETURN m.id as id, m.email as email, m.firstName as firstName, m.lastName as lastName
30+
LIMIT 1`,
31+
{ email },
32+
)
33+
34+
if (inactiveMemberResult.records.length > 0) {
35+
const inactiveMember = inactiveMemberResult.records[0]
36+
const memberId = inactiveMember.get('id')
37+
const memberFirstName = inactiveMember.get('firstName')
38+
const memberLastName = inactiveMember.get('lastName')
39+
40+
throw new ApiError(
41+
409,
42+
`An inactive account exists for ${memberFirstName} ${memberLastName}. Please contact support to reactivate your account.`,
43+
)
44+
}
45+
46+
// 2. Check for duplicate email (active members/users)
47+
const emailCheckResult = await session.run(
48+
`MATCH (u)
49+
WHERE (u:User OR u:Member) AND u.email = $email
50+
RETURN u.id as id, u.email as email, u.firstName as firstName, u.lastName as lastName
51+
LIMIT 1`,
2852
{ email },
2953
)
3054

31-
if (existingUser.records.length > 0) {
32-
throw new ApiError(400, 'A user with this email already exists')
55+
if (emailCheckResult.records.length > 0) {
56+
const existingUser = emailCheckResult.records[0]
57+
const existingFirstName = existingUser.get('firstName')
58+
const existingLastName = existingUser.get('lastName')
59+
60+
throw new ApiError(
61+
409,
62+
`There is already an account with this email "${email}" for ${existingFirstName} ${existingLastName}`,
63+
)
3364
}
3465

35-
// Hash password
66+
// 3. Check for duplicate WhatsApp number (if provided)
67+
if (whatsappNumber) {
68+
const whatsappCheckResult = await session.run(
69+
`MATCH (m:Member {whatsappNumber: $whatsappNumber})
70+
RETURN m.id as id, m.whatsappNumber as whatsappNumber, m.firstName as firstName, m.lastName as lastName
71+
LIMIT 1`,
72+
{ whatsappNumber },
73+
)
74+
75+
if (whatsappCheckResult.records.length > 0) {
76+
const existingMember = whatsappCheckResult.records[0]
77+
const existingFirstName = existingMember.get('firstName')
78+
const existingLastName = existingMember.get('lastName')
79+
80+
throw new ApiError(
81+
409,
82+
`There is already a member with this WhatsApp number "${whatsappNumber}" for ${existingFirstName} ${existingLastName}`,
83+
)
84+
}
85+
}
86+
87+
// 4. Hash password
3688
const hashedPassword = await hashPassword(password)
3789

38-
// Create user
90+
// 5. Create user
3991
const result = await session.run(
4092
`CREATE (person:User)
4193
SET person.id = randomUUID(),
4294
person.email = $email,
4395
person.password = $password,
4496
person.firstName = $firstName,
4597
person.lastName = $lastName,
98+
person.whatsappNumber = $whatsappNumber,
99+
person.markedInactive = false,
46100
person.createdAt = datetime(),
47101
person.updatedAt = datetime()
48-
RETURN person.id as id, person.email as email`,
102+
RETURN person.id as id, person.email as email, person.firstName as firstName, person.lastName as lastName`,
49103
{
50104
email,
51105
password: hashedPassword,
52-
firstName: firstName || '',
53-
lastName: lastName || '',
106+
firstName,
107+
lastName,
108+
whatsappNumber: whatsappNumber || null,
54109
},
55110
)
56111

@@ -62,17 +117,29 @@ export const signup = asyncHandler(async (req: Request, res: Response) => {
62117
const userId = record.get('id')
63118
const userEmail = record.get('email')
64119

65-
// Send welcome email (non-blocking)
66-
sendWelcomeEmail(userEmail, firstName || 'User').catch((error) => {
120+
// 6. Send welcome email (non-blocking)
121+
sendWelcomeEmail(userEmail, firstName).catch((error) => {
67122
console.error('Failed to send welcome email:', error)
68-
// Don't fail the request if email fails
69123
})
70124

125+
// 7. Log security event
126+
console.log(
127+
JSON.stringify({
128+
timestamp: new Date().toISOString(),
129+
event: 'user_signup',
130+
userId,
131+
email: userEmail,
132+
clientIP: req.headers['x-forwarded-for'] || req.ip,
133+
}),
134+
)
135+
71136
res.status(201).json({
72137
message: 'User created successfully',
73138
user: {
74139
id: userId,
75140
email: userEmail,
141+
firstName,
142+
lastName,
76143
},
77144
})
78145
} catch (error) {

0 commit comments

Comments
 (0)