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:
constsocket=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.