CryptWeb is a Node.js/Express backend service providing:
- Authentication (signup, login with JWT + cookie-based sessions)
- Email Verification (OTP code verification flow)
- Password Management (forgot + reset password with email token)
- Session Management (multi-device sessions, logout, token refresh)
- File Transfer Logging (persists completed P2P file transfer metadata)
- WebRTC Signaling (Socket.IO-based offer/answer/ICE exchange with active peer tracking)
- Health Check (application + database status)
Base URL: All REST routes are prefixed with /api.
Source: src/app.ts — app.use('/api', v1Router)
All responses use one of two standardized classes.
Source: src/utils/responses/ApiResponse.ts
{
"statusCode": 200,
"data": {},
"message": "Success message",
"success": true
}| Field | Type | Description |
|---|---|---|
statusCode |
number |
HTTP status code |
data |
T |
Response payload (type varies per endpoint) |
message |
string |
Human-readable summary |
success |
boolean |
true when statusCode < 400 |
Source: src/utils/responses/ApiError.ts
{
"statusCode": 400,
"data": null,
"message": "Error description",
"success": false,
"errors": ["Optional array of validation details"]
}| Field | Type | Description |
|---|---|---|
statusCode |
number |
HTTP status code |
data |
null |
Always null on errors |
message |
string |
Error description |
success |
boolean |
Always false |
errors |
any[] |
Optional. Validation error details |
Source: src/middlewares/auth.middleware.ts
Routes marked as ** Authenticated** require the following header:
Authorization: Bearer <accessToken>
Middleware behavior:
| Scenario | Status | Message |
|---|---|---|
No Authorization header |
400 |
Auth headers missing |
| Header present, no token | 401 |
Access token required |
| Token expired / invalid JWT | 401 |
Token expired |
| Unexpected error | 500 |
Something went wrong at our end. Please Try again later |
The middleware extracts sub from the JWT payload and attaches it as req.user.id.
Source: src/middlewares/rateLimitter.middleware.ts
| Limiter | Window | Max Requests | Applied To |
|---|---|---|---|
authLimiter |
15 minutes | 5 | POST /api/v1/auth/login |
signupLimiter |
10 hours | 20 | POST /api/v1/auth/signup |
healthLimiter |
15 minutes | 5 | GET /api/v1/health |
generalLimiter |
1 minute | 100 | Not found applied to any route in current code |
When exceeded, the response body is a plain string message (e.g., "Too many login attempts, please try again later").
Auth: None
Rate Limit: None
Source: src/app.ts
Response (200):
{
"statusCode": 200,
"data": { "version": "<API_VERSION from env>" },
"message": "Welcome to auth service backend",
"success": true
}Auth: None
Rate Limit: signupLimiter (20 req / 10 hours)
Source: src/controllers/auth.controller.ts → src/services/auth.service.ts
Request Body:
{
"name": "string",
"email": "string",
"password": "string"
}Validated using signupSchema (Zod). Fields: userName (mapped from name), email, password.
Response (201):
{
"statusCode": 201,
"data": {
"user": {
"id": "uuid",
"email": "string",
"name": "string",
"profile_picture": "string | undefined",
"created_on": "Date | undefined"
}
},
"message": "User created successfully",
"success": true
}A verification code email is automatically sent after signup.
Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Missing input fields |
Any of name/email/password missing |
400 |
Invalid inputs fields |
Zod validation failed (errors array contains details) |
409 |
Email already exists |
Email already registered |
500 |
Something went wrong... |
Unexpected server error |
Auth: None
Rate Limit: authLimiter (5 req / 15 min)
Source: src/controllers/auth.controller.ts → src/services/auth.service.ts
Request Body:
{
"email": "string",
"password": "string"
}Validated using loginSchema (Zod).
Response (200):
Sets three httpOnly, secure cookies: accessToken, refreshToken, deviceId.
{
"statusCode": 200,
"data": {
"user": {
"id": "uuid",
"email": "string",
"name": "string",
"profile_picture": "string | undefined",
"created_on": "Date | undefined"
},
"accessToken": "jwt string",
"refreshToken": "jwt string",
"deviceId": "hex string (20 chars)",
"sessionId": "uuid"
},
"message": "logged in successfully",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Email and Password required |
Missing fields |
400 |
Invalid fields |
Zod validation failed |
404 |
User not found |
Email not in database |
400 |
Invalid credentials |
Password mismatch |
500 |
There was unexpected error creating your session... |
Session creation failed |
500 |
Something went wrong... |
Unexpected server error |
Auth: None
Rate Limit: None
Source: src/controllers/verfiyUser.controller.ts → src/services/verify-email.service.ts
Request Body:
{
"email": "string",
"code": "string (6 characters)"
}Response (200):
{
"statusCode": 200,
"data": null,
"message": "User verified successfully",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Please enter 4 verification code |
Missing code, email, or code length ≠ 6 |
400 |
Invalid email address |
Email format validation failed |
404 |
User not found |
Email not in database |
404 |
No code found. Please signup or send click resend token |
No verification token exists |
200 |
Email already verified |
token.used_at is set (not an error, returns ApiResponse) |
400 |
Token Expired |
Code older than 5 minutes (OTP_EXPIRY_MS: 300000) |
400 |
Invalid code |
bcrypt compare fails |
500 |
Something went wrong... |
Unexpected error |
Auth: None
Rate Limit: None
Source: src/controllers/verfiyUser.controller.ts → src/services/verify-email.service.ts
Request Body:
{
"email": "string"
}Response (201):
{
"statusCode": 201,
"data": null,
"message": "Code send to email",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Email Required |
Empty email |
400 |
Invalid email address |
Format check failed |
404 |
User not found |
Email not in database |
200 |
User already verified |
user.verified_at is set (returns ApiResponse, not error) |
500 |
Something went wrong... |
Unexpected error |
Auth: None
Rate Limit: None
Source: src/controllers/resetPassword.controller.ts → src/services/reset-password.service.ts
Request Body:
{
"email": "string"
}Response (200):
{
"statusCode": 200,
"data": null,
"message": "If the email exists, a reset link has been sent.",
"success": true
}A reset token is emailed asynchronously via process.nextTick.
Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Invalid email address |
Format validation |
404 |
User not found |
Email not in database |
500 |
Error generating reset password token |
Token storage failed |
500 |
Something went wrong... |
Unexpected error |
Auth: None
Rate Limit: None
Source: src/controllers/resetPassword.controller.ts → src/services/reset-password.service.ts
Request Body:
{
"email": "string",
"password": "string",
"confirmPassword": "string",
"token": "string (received via email)"
}On success, all existing sessions for the user are invalidated (forced re-login).
Response (200):
{
"statusCode": 200,
"data": null,
"message": "Password reset successfull, Please Login again",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Email and password required |
Missing fields |
400 |
Invalid email address |
Format validation |
400 |
Password does not match |
password ≠ confirmPassword |
400 |
Invalid Password |
Zod passwordSchema failed |
200 |
If the email exists, a reset link has been sent. |
Email not found (ambiguous response by design) |
404 |
No active reset token found |
No token in database |
400 |
Token already used |
resetToken.used_at is set |
400 |
Reset Token Expired |
Token past expires_at |
400 |
Invalid Token |
Token hash comparison fails |
500 |
Something went wrong... |
Unexpected error |
Auth: Authenticated
Rate Limit: None
Source: src/controllers/userSessions.controller.ts → src/services/user-session.service.ts
Query Parameters:
| Param | Type | Required |
|---|---|---|
userId |
string (UUID) |
Yes |
Response (200):
{
"statusCode": 200,
"data": [
{
"id": "uuid",
"user_id": "uuid",
"device_id": "string",
"device_type": {
"browser": "string",
"os": "string",
"device": "string",
"vendor": "string",
"model": "string"
},
"refresh_token": "string",
"expires_at": "ISO date",
"created_at": "ISO date"
}
],
"message": "sessions fetched successfully",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
User id required |
Missing userId |
400 |
Invalid user id |
Not a valid UUID |
404 |
No user session found |
No sessions exist |
500 |
Something went wrong... |
Unexpected error |
Auth: Authenticated
Rate Limit: None
Source: src/controllers/userSessions.controller.ts → src/services/user-session.service.ts
Request Body:
{
"sessionId": "uuid",
"deviceId": "string"
}On success, clears cookies: accessToken, refreshToken, deviceId.
Response (200):
{
"statusCode": 200,
"data": "deleted session uuid",
"message": "Session deleted successfully",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Required fields missing |
Missing sessionId or deviceId |
400 |
Invalid user id |
sessionId not valid UUID |
400 |
No session found |
Session doesn't exist |
500 |
Something went wrong... |
Unexpected error |
Auth: Authenticated
Rate Limit: None
Source: src/controllers/userSessions.controller.ts → src/services/user-session.service.ts
Request Body: None. User ID is extracted from req.user.id (set by auth middleware).
Response (200):
{
"statusCode": 200,
"data": ["array of deleted session ids"],
"message": "Log out from all devices sucessfull",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Invalid user id |
UUID validation failed |
404 |
No active user sessions found |
No sessions to delete |
500 |
Something went wrong... |
Unexpected error |
Auth: None
Rate Limit: None
Source: src/controllers/userSessions.controller.ts → src/services/tokens.service.ts
Request Body:
{
"refreshToken": "string",
"userId": "uuid",
"deviceId": "string",
"sessionId": "uuid"
}Response (200):
{
"statusCode": 200,
"data": {
"accessToken": "new jwt string"
},
"message": "Access token generated successfully",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Bad Request, Required fields are empty |
Missing any field |
400 |
Invalid user id |
UUID validation failed |
404 |
User not found |
userId not in database |
404 |
No session found |
Session doesn't exist |
400 |
Refresh token expired |
Session expires_at passed |
400 |
Invalid refresh Token |
bcrypt compare fails |
500 |
Something went wrong... |
Unexpected error |
Auth: Authenticated
Rate Limit: None
Source: src/controllers/fileTransfers.controller.ts → src/services/fileTransfers.service.ts → src/repositories/file_transfers.repo.ts
Request Body:
{
"senderEmail": "string",
"receiverEmail": "string",
"fileName": "string",
"fileSize": 1048576,
"fileType": "application/pdf",
"timeElapsed": 4500,
"transferType": "WebRTC"
}| Field | Type | Description |
|---|---|---|
senderEmail |
string |
Email of file sender |
receiverEmail |
string |
Email of file receiver |
fileName |
string |
Name of the transferred file |
fileSize |
number |
File size in bytes |
fileType |
string |
MIME type |
timeElapsed |
number |
Transfer duration in milliseconds |
transferType |
string |
Transfer method (e.g., "WebRTC", "Relay") |
Response (201):
{
"statusCode": 201,
"data": {
"id": "uuid",
"sender": "uuid",
"receiver": "uuid",
"file_name": "string",
"file_size": 1048576,
"file_type": "string",
"time_elapsed": 4500,
"completed_at": "ISO date",
"transfer_type": "string"
},
"message": "File transfer recorded successfully",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
400 |
Invalid email address |
Email format validation failed |
400 |
Invalid file transfer data |
Missing fileName/fileSize/fileType/timeElapsed/transferType |
404 |
Sender or receiver not found |
Email not found in database |
500 |
Something went wrong... |
Unexpected error |
Auth: None
Rate Limit: healthLimiter (5 req / 15 min)
Source: src/controllers/health.controller.ts → src/services/health.service.ts
Response (200):
{
"statusCode": 200,
"data": {
"app": {
"status": "up",
"uptime": 123.45,
"memoryUsage": {
"rss": "45.23 MB",
"heapTotal": "30.12 MB",
"heapUsed": "25.67 MB",
"external": "1.23 MB"
}
},
"database": {
"status": "healthy",
"latency": "3 ms"
}
},
"message": "Health check successful",
"success": true
}Response (503) — Database Down:
{
"statusCode": 503,
"data": {
"app": { "status": "up", "uptime": 123.45, "memoryUsage": { "..." } },
"database": { "status": "down", "latency": "0 ms" }
},
"message": "Service unavailable",
"success": false
}Source: src/components/singalling.ts
The Socket.IO server runs on the same HTTP server as Express. Connect using:
const socket = io("http://localhost:<PORT>");No socket-level authentication middleware is found in the codebase. Registration is handled via the register event with database validation.
Direction: Client → Server
Source: singalling.ts lines 25–49
Payload:
{ "email": "string", "name": "string" }Behavior:
- If
nameoremailis missing → silently returns (no response emitted). - Queries the database via
Users.getByEmail(email). - If user not found → emits
registration-error. - If database query throws → emits
registration-error. - On success → adds to
emailToSocketMap(no response event emitted).
Possible emitted responses:
registration-error (Server → Client):
{ "message": "User does not exist" }{ "message": "Internal server error" }Direction: Client → Server (then forwarded Server → Client)
Source: singalling.ts lines 51–61
Client sends:
{
"from": "sender email",
"to": "target email",
"offer": "RTCSessionDescriptionInit"
}If target user is online — emits offer to target socket:
{
"offer": "RTCSessionDescriptionInit",
"from": "sender email"
}If target user is NOT online — emits user-status back to sender:
{ "isOnline": false, "message": "user offline" }Direction: Client → Server (then forwarded Server → Client)
Source: singalling.ts lines 63–72
Client sends:
{
"from": "sender email",
"to": "target email",
"answer": "RTCSessionDescriptionInit"
}If target user is online — emits answer to target socket:
{
"answer": "RTCSessionDescriptionInit",
"from": "sender email"
}If target user is NOT online — emits user-status back to sender:
{ "isOnline": false, "message": "user offline" }Direction: Client → Server (then forwarded Server → Client)
Source: singalling.ts lines 75–85
Client sends:
{
"from": "sender email",
"to": "target email",
"candidate": "RTCIceCandidateInit"
}If target user is online — emits ice-candidate to target socket:
{
"candidate": "RTCIceCandidateInit",
"from": "sender email"
}If target user is NOT online — emits user-status back to sender:
{ "isOnline": false, "message": "user offline" }Direction: Client → Server
Source: singalling.ts lines 87–94
Client sends:
{
"initiator": "email of the user who created the offer",
"receiver": "email of the user who received the offer"
}Behavior:
- Stores bidirectional mapping in
activePeersmap:initiator ↔ receiver. - No response event is emitted.
Direction: Automatic (triggered by Socket.IO on connection loss)
Source: singalling.ts lines 96–114
Behavior:
- Looks up the disconnected socket's email via
getEmailBySocketId. - If email not found → returns (no action).
- Checks
activePeersmap for a connected peer. - If no peer found → returns (only cleans up emailToSocketMap).
- If peer found → emits
user-statusto the peer's socket. - Cleans up both entries from
activePeersand removes fromemailToSocketMap.
Emitted to peer (user-status, Server → Client):
{
"isOnline": false,
"message": "<name> went offline"
}Where <name> is the registered name of the disconnected user, or "User" if lookup fails.
| Event Name | When Emitted |
|---|---|
registration-error |
register fails (user not in DB or internal error) |
offer |
Forwarded from another peer |
answer |
Forwarded from another peer |
ice-candidate |
Forwarded from another peer |
user-status |
Target user offline (on offer/answer/ice-candidate) OR active peer disconnects |
Source: singalling.ts lines 17–20
| Map | Key Type | Value Type | Purpose |
|---|---|---|---|
emailToSocketMap |
string (email) |
{ socketId: string, name: string } |
Maps registered emails to socket IDs |
activePeers |
string (email) |
string (peer email) |
Tracks active P2P connections |
1. POST /api/v1/auth/signup → Creates user, sends OTP email
2. POST /api/v1/verify/email → Verifies OTP code
3. POST /api/v1/auth/login → Returns tokens + cookies + session
1. POST /api/v1/password/forgot → Sends reset token via email
2. POST /api/v1/password/reset → Validates token, updates password, invalidates ALL sessions
3. POST /api/v1/auth/login → User must re-login
1. POST /api/v1/user-session/get-access-token → Send refreshToken + userId + deviceId + sessionId
2. Receive new accessToken in response
1. Both clients connect via Socket.IO
2. Both emit "register" with { email, name } → server validates against DB
3. Initiator emits "offer" → server forwards to receiver
4. Receiver emits "answer" → server forwards to initiator
5. Both exchange "ice-candidate" events
6. Once WebRTC connection established, either side emits "users:connected"
7. On disconnect, server automatically notifies the peer via "user-status"
1. File transfer happens over WebRTC DataChannel (not implemented in backend)
2. After completion, client calls POST /api/v1/file-transfers/complete
3. Server resolves emails to user UUIDs and persists the record
Source: database/001_init.sql
| Column | Type | Constraints |
|---|---|---|
id |
UUID |
PK, default gen_random_uuid() |
name |
VARCHAR(30) |
NOT NULL |
email |
VARCHAR(255) |
NOT NULL, UNIQUE |
password_hash |
TEXT |
NOT NULL |
last_login_at |
TIMESTAMPTZ |
|
profile_picture |
TEXT |
|
verified_at |
TIMESTAMPTZ |
|
deleted_at |
TIMESTAMPTZ |
|
created_on |
TIMESTAMPTZ |
NOT NULL, DEFAULT NOW() |
updated_on |
TIMESTAMPTZ |
NOT NULL, DEFAULT NOW() |
| Column | Type | Constraints |
|---|---|---|
id |
UUID |
PK |
user_id |
UUID |
FK → users(id), ON DELETE CASCADE |
device_id |
TEXT |
UNIQUE, NOT NULL |
refresh_token |
TEXT |
NOT NULL |
expires_at |
TIMESTAMPTZ |
NOT NULL |
created_at |
TIMESTAMPTZ |
DEFAULT NOW() |
device_type |
JSONB |
DEFAULT {} |
| Column | Type | Constraints |
|---|---|---|
id |
UUID |
PK |
user_id |
UUID |
NOT NULL, UNIQUE, FK → users(id) |
token_hash |
TEXT |
NOT NULL |
used_at |
TIMESTAMPTZ |
DEFAULT NULL |
revoked_at |
TIMESTAMPTZ |
DEFAULT NULL |
created_at |
TIMESTAMPTZ |
DEFAULT NOW() |
expires_at |
TIMESTAMPTZ |
NOT NULL |
| Column | Type | Constraints |
|---|---|---|
id |
UUID |
PK |
user_id |
UUID |
NOT NULL, FK → users(id) |
expires_at |
TIMESTAMPTZ |
NOT NULL |
created_at |
TIMESTAMPTZ |
DEFAULT NOW() |
token_hash |
TEXT |
NOT NULL |
used_at |
TIMESTAMPTZ |
DEFAULT NULL |
| Column | Type | Constraints |
|---|---|---|
id |
UUID |
PK |
sender |
UUID |
NOT NULL, FK → users(id) |
receiver |
UUID |
NOT NULL, FK → users(id) |
file_name |
TEXT |
NOT NULL |
file_size |
BIGINT |
NOT NULL |
file_type |
TEXT |
NOT NULL |
time_elapsed |
INTEGER |
NOT NULL |
completed_at |
TIMESTAMPTZ |
DEFAULT NULL |
transfer_type |
TEXT |
NOT NULL |
| Scenario | Where Handled | Response |
|---|---|---|
| Missing required fields | All services | 400 with specific message |
| Invalid email format | Verify, Reset, FileTransfer services | 400 Invalid email address |
| Invalid UUID format | Session service, Token service | 400 Invalid user id |
| User not found by email | Auth, Verify, Reset, Signaling | 404 User not found |
| User not found by ID | Token service | 404 User not found |
| Duplicate email on signup | Auth service | 409 Email already exists |
| Password mismatch on login | Auth service | 400 Invalid credentials |
| Password ≠ confirmPassword on reset | Reset service | 400 Password does not match |
| Expired OTP code | Verify service | 400 Token Expired |
| Already verified email | Verify service | 200 Email already verified |
| Reset token already used | Reset service | 400 Token already used |
| Reset token expired | Reset service | 400 Reset Token Expired |
| Refresh token expired | Token service | 400 Refresh token expired |
| Invalid refresh token hash | Token service | 400 Invalid refresh Token |
| No active sessions found | Session service | 404 No user session found |
| Target user offline (socket) | Signaling (offer/answer/ice) | user-status event emitted |
| Unregistered email on socket register | Signaling | registration-error event |
| Active peer disconnects | Signaling (disconnect handler) | user-status event to peer |
| Rate limit exceeded | Auth login, signup, health | 429 with text message |
Source: src/constants.ts
{
"httpOnly": true,
"secure": true
}Cookies set on login: accessToken, refreshToken, deviceId.
Cookies cleared on logout: accessToken, refreshToken, deviceId.