Skip to content

Commit 0b06feb

Browse files
author
Lasim
committed
feat: implement email verification system
- Added email verification template in Pug format for sending verification emails. - Created route for resending verification emails with appropriate response handling. - Implemented email verification route to verify user email using a token. - Developed email verification service to manage token generation, hashing, and verification. - Added frontend localization for email verification messages and errors. - Created Vue component for verifying email and resending verification emails with user feedback.
1 parent 3d9d7bd commit 0b06feb

File tree

22 files changed

+2113
-18
lines changed

22 files changed

+2113
-18
lines changed

services/backend/SECURITY.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,10 +36,27 @@ User sessions are managed using `lucia-auth` v3.
3636
All incoming data from clients (e.g., API request bodies, URL parameters) is rigorously validated using `zod` schemas on the server-side before being processed. This helps prevent common vulnerabilities such as injection attacks and unexpected data handling errors.
3737

3838
- Registration endpoint validates: username, email, password, first_name, last_name
39+
- **Email verification endpoint validates: verification token format and expiration**
3940
- Email addresses are normalized to lowercase before storage
4041
- Duplicate username and email checks are performed before user creation
42+
- **Email verification status is checked during login when verification is enabled**
4143
- All database operations use parameterized queries via Drizzle ORM to prevent SQL injection
4244

45+
## Email Verification
46+
47+
Email verification is implemented to ensure account ownership and prevent unauthorized account creation.
48+
49+
- **Verification Tokens:** Cryptographically secure tokens generated using `generateId(32)` (256-bit entropy)
50+
- **Token Expiration:** Verification tokens expire after 24 hours to limit exposure window
51+
- **Token Storage:** Tokens are stored hashed in the database using the same argon2 parameters as passwords
52+
- **Secure Links:** Verification links use HTTPS in production and include the full token in query parameters
53+
- **Token Validation:** Constant-time comparison used for token verification to prevent timing attacks
54+
- **Single Use:** Tokens are invalidated immediately after successful verification
55+
- **Account Security:** Unverified accounts cannot log in when email verification is enabled via `global.send_mail` setting
56+
- **Global Administrator Exception:** The first registered user (global administrator) is automatically verified for system access
57+
- **Database Schema Security:** `email_verified` boolean field with secure default (false)
58+
- **Cleanup Mechanism:** Expired tokens are automatically cleaned up to prevent database bloat
59+
4360
## Global Settings Encryption
4461

4562
Sensitive global application settings (SMTP credentials, API keys, etc.) are encrypted at rest using industry-standard encryption.

services/backend/api-spec.json

Lines changed: 240 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6437,6 +6437,246 @@
64376437
}
64386438
}
64396439
},
6440+
"/api/auth/email/verify": {
6441+
"get": {
6442+
"summary": "Verify email address",
6443+
"tags": [
6444+
"Authentication"
6445+
],
6446+
"description": "Verifies a user's email address using a verification token sent via email. This endpoint is public and does not require authentication. Once verified, the user's email_verified status is set to true.",
6447+
"parameters": [
6448+
{
6449+
"schema": {
6450+
"type": "string",
6451+
"minLength": 1
6452+
},
6453+
"in": "query",
6454+
"name": "token",
6455+
"required": true
6456+
}
6457+
],
6458+
"responses": {
6459+
"200": {
6460+
"description": "Email verified successfully",
6461+
"content": {
6462+
"application/json": {
6463+
"schema": {
6464+
"type": "object",
6465+
"properties": {
6466+
"success": {
6467+
"type": "boolean",
6468+
"description": "Indicates if the verification was successful"
6469+
},
6470+
"message": {
6471+
"type": "string",
6472+
"description": "Success message"
6473+
},
6474+
"userId": {
6475+
"type": "string",
6476+
"description": "ID of the verified user"
6477+
}
6478+
},
6479+
"required": [
6480+
"success",
6481+
"message",
6482+
"userId"
6483+
],
6484+
"additionalProperties": false,
6485+
"description": "Email verified successfully"
6486+
}
6487+
}
6488+
}
6489+
},
6490+
"400": {
6491+
"description": "Bad Request - Invalid or expired token",
6492+
"content": {
6493+
"application/json": {
6494+
"schema": {
6495+
"type": "object",
6496+
"properties": {
6497+
"success": {
6498+
"type": "boolean",
6499+
"description": "Indicates if the operation was successful (false for errors)",
6500+
"default": false
6501+
},
6502+
"error": {
6503+
"type": "string",
6504+
"description": "Error message describing what went wrong"
6505+
}
6506+
},
6507+
"required": [
6508+
"error"
6509+
],
6510+
"additionalProperties": false,
6511+
"description": "Bad Request - Invalid or expired token"
6512+
}
6513+
}
6514+
}
6515+
},
6516+
"500": {
6517+
"description": "Internal Server Error - Verification failed",
6518+
"content": {
6519+
"application/json": {
6520+
"schema": {
6521+
"type": "object",
6522+
"properties": {
6523+
"success": {
6524+
"type": "boolean",
6525+
"description": "Indicates if the operation was successful (false for errors)",
6526+
"default": false
6527+
},
6528+
"error": {
6529+
"type": "string",
6530+
"description": "Error message describing what went wrong"
6531+
}
6532+
},
6533+
"required": [
6534+
"error"
6535+
],
6536+
"additionalProperties": false,
6537+
"description": "Internal Server Error - Verification failed"
6538+
}
6539+
}
6540+
}
6541+
}
6542+
}
6543+
}
6544+
},
6545+
"/api/auth/email/resend-verification": {
6546+
"post": {
6547+
"summary": "Resend email verification",
6548+
"tags": [
6549+
"Authentication"
6550+
],
6551+
"description": "Resends a verification email to the specified email address. This endpoint is public and does not require authentication. Only works if the email address exists and is not already verified.",
6552+
"requestBody": {
6553+
"content": {
6554+
"application/json": {
6555+
"schema": {
6556+
"type": "object",
6557+
"properties": {
6558+
"email": {
6559+
"type": "string",
6560+
"format": "email"
6561+
}
6562+
},
6563+
"required": [
6564+
"email"
6565+
],
6566+
"additionalProperties": false
6567+
}
6568+
}
6569+
},
6570+
"required": true
6571+
},
6572+
"responses": {
6573+
"200": {
6574+
"description": "Verification email sent successfully",
6575+
"content": {
6576+
"application/json": {
6577+
"schema": {
6578+
"type": "object",
6579+
"properties": {
6580+
"success": {
6581+
"type": "boolean",
6582+
"description": "Indicates if the resend was successful"
6583+
},
6584+
"message": {
6585+
"type": "string",
6586+
"description": "Success message"
6587+
}
6588+
},
6589+
"required": [
6590+
"success",
6591+
"message"
6592+
],
6593+
"additionalProperties": false,
6594+
"description": "Verification email sent successfully"
6595+
}
6596+
}
6597+
}
6598+
},
6599+
"400": {
6600+
"description": "Bad Request - Email not found or already verified",
6601+
"content": {
6602+
"application/json": {
6603+
"schema": {
6604+
"type": "object",
6605+
"properties": {
6606+
"success": {
6607+
"type": "boolean",
6608+
"description": "Indicates if the operation was successful (false for errors)",
6609+
"default": false
6610+
},
6611+
"error": {
6612+
"type": "string",
6613+
"description": "Error message describing what went wrong"
6614+
}
6615+
},
6616+
"required": [
6617+
"error"
6618+
],
6619+
"additionalProperties": false,
6620+
"description": "Bad Request - Email not found or already verified"
6621+
}
6622+
}
6623+
}
6624+
},
6625+
"403": {
6626+
"description": "Forbidden - Email sending is disabled",
6627+
"content": {
6628+
"application/json": {
6629+
"schema": {
6630+
"type": "object",
6631+
"properties": {
6632+
"success": {
6633+
"type": "boolean",
6634+
"description": "Indicates if the operation was successful (false for errors)",
6635+
"default": false
6636+
},
6637+
"error": {
6638+
"type": "string",
6639+
"description": "Error message describing what went wrong"
6640+
}
6641+
},
6642+
"required": [
6643+
"error"
6644+
],
6645+
"additionalProperties": false,
6646+
"description": "Forbidden - Email sending is disabled"
6647+
}
6648+
}
6649+
}
6650+
},
6651+
"500": {
6652+
"description": "Internal Server Error - Failed to send email",
6653+
"content": {
6654+
"application/json": {
6655+
"schema": {
6656+
"type": "object",
6657+
"properties": {
6658+
"success": {
6659+
"type": "boolean",
6660+
"description": "Indicates if the operation was successful (false for errors)",
6661+
"default": false
6662+
},
6663+
"error": {
6664+
"type": "string",
6665+
"description": "Error message describing what went wrong"
6666+
}
6667+
},
6668+
"required": [
6669+
"error"
6670+
],
6671+
"additionalProperties": false,
6672+
"description": "Internal Server Error - Failed to send email"
6673+
}
6674+
}
6675+
}
6676+
}
6677+
}
6678+
}
6679+
},
64406680
"/api/auth/profile/update": {
64416681
"put": {
64426682
"summary": "Update user profile",

0 commit comments

Comments
 (0)