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)
- File Transfer History (retrieve recent transfers for the authenticated user)
- WebRTC Signaling (Socket.IO-based offer/answer/ICE exchange with active peer tracking)
- Network Discovery (Socket.IO rooms grouped by client IP + REST endpoint for LAN IP and online users)
- 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 (converted to MB on save) |
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: Authenticated
Rate Limit: None
Source: src/controllers/fileTransfers.controller.ts → src/services/fileTransfers.service.ts → src/repositories/file_transfers.repo.ts
Query Parameters:
| Param | Type | Default | Max | Description |
|---|---|---|---|---|
limit |
number |
10 | 50 | Number of transfers to return |
Response (200):
{
"statusCode": 200,
"data": [
{
"id": "uuid",
"fileSize": 0.01,
"fileType": "text/markdown",
"timeElapsed": 0.045,
"transferType": "send",
"completedAt": "2026-07-18T12:28:09.034Z",
"senderName": "Alice",
"senderEmail": "alice@example.com",
"receiverName": "Bob",
"receiverEmail": "bob@example.com"
}
],
"message": "Recent transfers fetched",
"success": true
}Errors:
| Status | Message | Condition |
|---|---|---|
401 |
Unauthorized |
Not authenticated |
500 |
Something went wrong... |
Unexpected error |
Auth: None
Rate Limit: None
Source: src/app.ts
Returns the server's LAN IP and all online users on the requesting client's network.
Response (200):
{
"statusCode": 200,
"data": {
"ip": "192.168.1.100",
"onlineUsers": [
{ "email": "alice@example.com", "name": "Alice" },
{ "email": "bob@example.com", "name": "Bob" }
]
},
"message": "Local network IP",
"success": true
}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/signalling.ts
The Socket.IO server runs on the same HTTP server as Express. Connect using:
const socket = io("http://localhost:<PORT>");Registration is handled via the user:register event with database validation.
Client IP is detected via x-forwarded-for header (trust proxy) or socket.handshake.address, normalized for both IPv4 and IPv6, and used to group users into network rooms for local discovery.
Shared State: Socket state (emailToSocketMap, activePeers, ipToUsersMap) lives in src/utils/networkStore.ts and is also accessible from REST endpoints.
Direction: Client → Server
Payload:
{ "email": "string", "name": "string" }Behavior:
- If
nameoremailis missing → emitsregistration-error. - Queries the database via
Users.getByEmail(email). - If user not found → emits
registration-error. - On success → adds to
emailToSocketMap, joins socket to roomnetwork:<normalizedIP>, adds toipToUsersMap, and broadcastsnetwork:user-joinedto all other sockets on the same IP.
Emitted responses:
registration-error:
{ "isOnline": null, "name": "<email>", "userExists": null, "message": "..." }network:user-joined (broadcast to network room):
{
"email": "string",
"name": "string",
"onlineUsers": [{ "email": "string", "name": "string" }]
}Direction: Client → Server
Payload:
{ "from": "sender email", "to": "target email" }Behavior:
- Validates target user exists in database.
- If target is online → emits
status-update(isOnline: true) to sender and forwardsconnection:incomingto target. - If target is offline → emits
status-update(isOnline: false) to sender.
Direction: Client → Server
Payload:
{ "from": "string", "to": "string", "accepted": true }Behavior:
- Forwards the response to the initiator. No peer tracking on this event.
Direction: Client → Server
Payload:
{ "initiator": "string", "receiver": "string" }Behavior:
- Stores bidirectional mapping in
activePeersmap:initiator ↔ receiver.
Direction: Client → Server
Payload: (none)
Response:
network:users (Server → Client):
[
{ "email": "string", "name": "string" }
]Returns all registered users connected from the same IP.
Direction: Client → Server (forwarded Server → Client)
Client sends:
{
"from": "sender email",
"to": "target email",
"offer"/"answer"/"candidate": "..."
}If target is online → forwards payload to target.
If target is offline → emits user-status back to sender:
{ "isOnline": false, "message": "user offline" }Behavior:
- Looks up the disconnected socket's email via
getEmailBySocketId. - If connected to an active peer → emits
peer:disconnectedto the peer's socket and cleans upactivePeers. - If registered on a network IP → removes from
ipToUsersMapand broadcastsnetwork:user-leftto the remaining room members. - Removes from
emailToSocketMap.
Emitted to peer (peer:disconnected):
{
"name": "string",
"email": "string",
"message": "<name> went offline. Try again later"
}Emitted to network room (network:user-left):
{
"email": "string",
"onlineUsers": [{ "email": "string", "name": "string" }]
}| Event Name | When Emitted |
|---|---|
registration-error |
user:register fails (user not in DB or internal error) |
network:user-joined |
A new user registered on the same network IP |
network:user-left |
A user on the same network IP disconnected |
network:users |
Response to a network:users request |
status-update |
Response to connection:request (online/offline status) |
connection:incoming |
Forwarded to target when someone requests a connection |
connection:response |
Forwarded to initiator with the responder's decision |
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) |
peer:disconnected |
An active peer disconnected |
Source: src/utils/networkStore.ts
| 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 |
ipToUsersMap |
string (IP) |
Set<string> (emails) |
Groups connected users by their client IP |
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/session/renew → Send refreshToken + userId + deviceId + sessionId
2. Receive new accessToken in response
1. Both clients connect via Socket.IO
2. Both emit "user:register" with { email, name } → server validates against DB, joins network room
3. Initiator emits "connection:request" → server checks online status, forwards to target
4. Target responds with "connection:response" → server forwards back to initiator
5. Initiator emits "offer" → server forwards to receiver
6. Receiver emits "answer" → server forwards to initiator
7. Both exchange "ice-candidate" events
8. Once WebRTC connection established, either side emits "users:connected"
9. On disconnect, server emits "peer:disconnected" to the connected peer
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
4. Client can retrieve transfer history via GET /api/v1/file-transfers/recent
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_size |
NUMERIC(10,2) |
NOT NULL (in MB) |
file_type |
TEXT |
NOT NULL |
time_elapsed |
DOUBLE PRECISION |
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) | peer:disconnected event to peer |
| Network user joins | Signaling (user:register) |
network:user-joined broadcast |
| Network user leaves | Signaling (disconnect handler) | network:user-left broadcast |
IPv4-mapped IPv6 (::ffff:...) |
normalizeIP in networkStore.ts |
Stripped to IPv4 |
| 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.