auth/Documentaion/database/OTP_TABLE_ANALYSIS.md

13 KiB

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

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):

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):

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

const { code } = await createOtp(normalizedPhone);

Function: src/services/otpService.js lines 82-117

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

const result = await verifyOtp(normalizedPhone, code);

Function: src/services/otpService.js lines 131-220

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

// ← 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!