auth/OTP_TABLE_ANALYSIS.md

317 lines
13 KiB
Markdown

# OTP Tables Analysis - What Your Code Actually Does
## Summary: **NO `user_id` in OTP Tables - This is CORRECT**
Your code uses **phone number only** for OTP tables. Users are created **AFTER** OTP verification.
---
## Database Tables
### 1. `otp_requests` Table (in `init.sql` lines 94-109)
**Status: ❌ NOT USED BY YOUR CODE**
```sql
CREATE TABLE IF NOT EXISTS otp_requests (
id UUID PRIMARY KEY,
phone_number VARCHAR(20) NOT NULL, -- NO user_id!
otp_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
consumed_at TIMESTAMPTZ, -- Extra field for tracking consumption
attempt_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
**Note:** This table exists in your database schema but is **never referenced in your code**.
---
### 2. `otp_codes` Table (ACTUALLY USED)
**Status: ✅ ACTIVELY USED BY YOUR CODE**
#### Database Schema (`init.sql` lines 115-122):
```sql
CREATE TABLE IF NOT EXISTS otp_codes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number VARCHAR(20) NOT NULL, -- NO user_id!
otp_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempt_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
#### Code Definition (`src/services/otpService.js` lines 34-41):
```javascript
CREATE TABLE IF NOT EXISTS otp_codes (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
phone_number VARCHAR(20) NOT NULL, // ← NO user_id!
otp_hash VARCHAR(255) NOT NULL,
expires_at TIMESTAMPTZ NOT NULL,
attempt_count INT NOT NULL DEFAULT 0,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
```
**Both schemas match - NO `user_id` column!**
---
## How Your Code Uses `otp_codes`
### Step 1: Request OTP (`/auth/request-otp`)
**Location:** `src/routes/authRoutes.js` line 189
```javascript
const { code } = await createOtp(normalizedPhone);
```
**Function:** `src/services/otpService.js` lines 82-117
```javascript
async function createOtp(phoneNumber) {
await ensureOtpCodesTable();
const code = generateOtpCode();
const expiresAt = new Date(Date.now() + OTP_EXPIRY_MS);
const otpHash = await bcrypt.hash(code, 10);
// Encrypt phone number before storing
const encryptedPhone = encryptPhoneNumber(phoneNumber);
// Delete any existing OTPs for this phone number
await db.query(
'DELETE FROM otp_codes WHERE phone_number = $1 OR phone_number = $2',
[encryptedPhone, phoneNumber]
);
// ← INSERT: Only phone_number, NO user_id!
await db.query(
`INSERT INTO otp_codes (phone_number, otp_hash, expires_at, attempt_count)
VALUES ($1, $2, $3, 0)`,
[encryptedPhone, otpHash, expiresAt] // ← NO user_id here!
);
return { code };
}
```
**What happens:**
- ✅ User requests OTP with **phone number only**
- ✅ OTP stored in `otp_codes` with **phone_number only**
-**NO user_id** because user doesn't exist yet!
---
### Step 2: Verify OTP (`/auth/verify-otp`)
#### Part A: Verify OTP Code
**Location:** `src/routes/authRoutes.js` line 267
```javascript
const result = await verifyOtp(normalizedPhone, code);
```
**Function:** `src/services/otpService.js` lines 131-220
```javascript
async function verifyOtp(phoneNumber, code) {
await ensureOtpCodesTable();
const encryptedPhone = encryptPhoneNumber(phoneNumber);
// ← SELECT: Lookup by phone_number only!
const result = await db.query(
`SELECT id, otp_hash, expires_at, attempt_count, phone_number
FROM otp_codes
WHERE phone_number = $1 OR phone_number = $2 // ← NO user_id in WHERE clause!
ORDER BY created_at DESC
LIMIT 1`,
[encryptedPhone, phoneNumber]
);
// ... verification logic ...
// ← DELETE: Remove OTP after verification
await db.query('DELETE FROM otp_codes WHERE id = $1', [otpRecord.id]);
return { ok: true };
}
```
**What happens:**
- ✅ OTP verified using **phone_number only**
- ✅ OTP deleted from `otp_codes` table
-**Still NO user_id** at this point!
---
#### Part B: Create/Find User (AFTER OTP Verification)
**Location:** `src/routes/authRoutes.js` lines 310-335
```javascript
// ← This happens AFTER OTP verification succeeds!
// find or create user
const encryptedPhone = encryptPhoneNumber(normalizedPhone);
const phoneSearchParams = preparePhoneSearchParams(normalizedPhone);
let user;
const found = await db.query(
`SELECT id, phone_number, name, role, NULL::user_type_enum as user_type,
COALESCE(token_version, 1) as token_version
FROM users
WHERE phone_number = $1 OR phone_number = $2`,
phoneSearchParams
);
if (found.rows.length === 0) {
// ← CREATE USER HERE (after OTP verified!)
const inserted = await db.query(
`INSERT INTO users (phone_number) // ← Only phone_number, user gets auto-generated UUID id
VALUES ($1)
RETURNING id, phone_number, name, role, NULL::user_type_enum as user_type,
COALESCE(token_version, 1) as token_version`,
[encryptedPhone]
);
user = inserted.rows[0]; // ← user.id is created HERE!
} else {
user = found.rows[0]; // ← Existing user found
}
// Now user.id exists!
```
**What happens:**
-**AFTER** OTP verification succeeds
- ✅ User is **found or created** based on phone_number
-`user.id` (UUID) is **assigned at this point**
- ✅ This is when user_id first exists!
---
## Complete Flow Diagram
```
┌─────────────────────────────────────────────────────────────┐
│ STEP 1: Request OTP │
│ POST /auth/request-otp │
│ Body: { phone_number: "+919876543210" } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ INSERT INTO otp_codes │
│ (phone_number, otp_hash, expires_at, attempt_count) │
│ VALUES ('+919876543210', 'hash...', '2024-...', 0) │
│ │
│ ❌ NO user_id - User doesn't exist yet! │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ STEP 2: Verify OTP │
│ POST /auth/verify-otp │
│ Body: { phone_number: "+919876543210", code: "123456" } │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ SELECT FROM otp_codes │
│ WHERE phone_number = '+919876543210' │
│ │
│ ❌ NO user_id in query - phone_number only! │
└─────────────────────────────────────────────────────────────┘
▼ (if OTP valid)
┌─────────────────────────────────────────────────────────────┐
│ DELETE FROM otp_codes WHERE id = ... │
│ (OTP consumed) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ STEP 3: Create/Find User │
│ (AFTER OTP verification succeeds) │
│ │
│ SELECT FROM users WHERE phone_number = '+919876543210' │
│ │
│ If NOT found: │
│ INSERT INTO users (phone_number) │
│ VALUES ('+919876543210') │
│ RETURNING id ← user.id CREATED HERE! │
│ │
│ ✅ user.id EXISTS NOW! │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ STEP 4: Create Session │
│ │
│ INSERT INTO refresh_tokens │
│ (user_id, token_hash, device_id, ...) │
│ VALUES (user.id, ...) ← user_id used here! │
│ │
│ INSERT INTO user_devices │
│ (user_id, device_identifier, ...) │
│ VALUES (user.id, ...) ← user_id used here! │
└─────────────────────────────────────────────────────────────┘
```
---
## Why NO `user_id` in OTP Tables?
### ✅ Current Design (CORRECT):
1. **User doesn't exist when OTP is requested**
- OTP can be requested for phone numbers that don't have accounts yet
- User is created **after** successful OTP verification
2. **Phone number is the identifier**
- Phone number is UNIQUE in `users` table
- Phone number links OTP → User
- No need for user_id in OTP table
3. **Supports both registration and login**
- New users: OTP → Create user
- Existing users: OTP → Find user
- Same flow works for both!
### ❌ Alternative (Would Be Wrong):
If you added `user_id` to `otp_codes`:
- ❌ Can't request OTP for new users (user_id doesn't exist)
- ❌ Would need to create user before OTP (defeats purpose of verification)
- ❌ Complicates the flow unnecessarily
---
## Summary Table
| Table | Has `user_id`? | When Used | Purpose |
|-------|---------------|-----------|---------|
| `otp_codes` | ❌ **NO** | Request/Verify OTP | Store OTP codes before user exists |
| `otp_requests` | ❌ **NO** | ❌ **NOT USED** | Legacy table (ignore it) |
| `users` | ✅ **YES** (primary key) | After OTP verification | Store user accounts |
| `refresh_tokens` | ✅ **YES** | After OTP verification | Store sessions (needs user_id) |
| `user_devices` | ✅ **YES** | After OTP verification | Track devices (needs user_id) |
---
## Answer to Your Question
**Q: Should `otp_code` and `otp_req` include user_id?**
**A: ❌ NO** - Your current code is **correct as-is**:
1. **`otp_codes`** - Does NOT have `user_id` ✅ (correct)
2. **`otp_requests`** - Does NOT have `user_id`, but also **NOT USED** by your code
**Q: Are we going to create a user ID before and assign it, or only use phone number?**
**A:** Your code uses **phone number only** for OTP, then creates user_id **AFTER** OTP verification:
- OTP request/verify: Uses **phone_number only**
- User creation: Happens **AFTER** OTP verification succeeds
- User_id assignment: Happens when user is created/found in `users` table
This is the **standard and secure** pattern for phone-based authentication! ✅