Skip to content

User Service - Complete Developer Documentation

Service: User Management Service
Port: 8002
Purpose: User signup, OTP verification, password management, feature assignment
Technology: FastAPI (Python 3.9+)
Code Location: /user-service/src/main.py (444 lines, 5 endpoints)
Owner: Backend Team
Last Updated: 2025-12-26


Table of Contents

  1. Service Overview
  2. Architecture & Responsibilities
  3. Complete Endpoints
  4. Signup Flow Deep-Dive
  5. OTP System
  6. Email Integration (Azure)
  7. Password Reset Flow
  8. Feature Assignment System
  9. Database Schemas
  10. Security Analysis
  11. Deployment

Service Overview

The User Service handles all user account management operations except login (which is handled by Auth Service). It's responsible for the complete user lifecycle from signup to subscription management.

Key Responsibilities

User Signup - Create new accounts
OTP Generation & Verification - Email-based verification
Email Sending - Azure Communication Services integration
Password Reset - Secure password recovery
Feature Assignment - Assign subscription features to users
User Feature Retrieval - Get user's subscription details

Statistics

  • Total Lines: 444
  • Endpoints: 5 main endpoints
  • Dependencies: FastAPI, PyMongo, Azure Communication Email
  • Email Provider: Azure Communication Services
  • Average Response Time: 100-200ms (2-5 seconds with email sending)

Architecture & Responsibilities

Position in System

graph TB
    subgraph "Client"
        A[Frontend]
    end

    subgraph "Gateway"
        B[Gateway<br/>Port 8000]
    end

    subgraph "Services"
        C[Auth Service<br/>Port 8001<br/>LOGIN]
        D[User Service<br/>Port 8002<br/>SIGNUP, OTP, RESET]
    end

    subgraph "External"
        E[Azure Communication<br/>Email Service]
    end

    subgraph "Database"
        F[(Cosmos DB)]
    end

    A -->|POST /v2/signup| B
    B --> D
    D -->|Send OTP| E
    D -->|Create user| F

    A -->|POST /v2/verify-otp| B
    B --> D
    D -->|Update verified| F
    D -->|Assign features| F

    A -->|POST /v2/login| B
    B --> C
    C -->|Validate| F

    style D fill:#E3F2FD
    style C fill:#FFE082

Division from Auth Service

Function Auth Service (8001) User Service (8002)
Login ✅ Validate & issue JWT
Signup ✅ Create user + OTP
OTP Generation ✅ 6-digit OTP
Email Sending ✅ Azure Communication
OTP Verification ✅ Verify + activate
Password Reset ✅ OTP-based reset
Feature Assignment ✅ Free plan features
JWT Creation ✅ Primary ✅ Also has helper

Complete Endpoints

1. POST /v2/signup

Purpose: Create new user account and send OTP for verification

Code Location: Lines 75-131

Request:

async def signup(
    email: str = Form(...),
    password: str = Form(...),
    name: str = Form(...)
)

Response (Success):

{
  "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
}

JWT Payload:

{
    "email": "user@example.com",
    "otp_sent": true,
    "exp": 1735214400
}

2. POST /v2/verify-otp

Purpose: Verify OTP and activate user account

Code Location: Lines 138-182

Request:

async def verify_otp(
    email: str = Form(...),
    otp: str = Form(...)
)

Response (Success):

{
  "message": "OTP verified successfully",
  "user_id": "User-123456"
}

3. POST /v2/forgot-password

Purpose: Initiate password reset (send OTP)

Code Location: Lines 241-260

Request:

async def forgot_password(email: str = Form(...))

Response:

{
  "token": "eyJhbGciOi..."
}

4. POST /v2/reset-password

Purpose: Reset password after OTP verification

Code Location: Lines 263-283

Request:

async def reset_password(
    email: str = Form(...),
    otp: str = Form(...),
    new_password: str = Form(...)
)

Response:

{
  "token": "eyJhbGciOi..."
}

5. GET /v2/get-user-features/{user_id}

Purpose: Get user's subscription features

Code Location: Lines 286-422

Request:

GET /v2/get-user-features/User-123456

Response:

{
    "user_features": {
        "user_id": "User-123456",
        "subscription_id": "sub_009",
        "no_of_chatbots": 1,
        "no_of_chat_sessions": 50,
        "no_of_linkcrawls": "50",
        "no_of_users": "1",
        "grace_period": "14",
        "features": [...]
    }
}

Signup Flow Deep-Dive

Complete Signup Process

sequenceDiagram
    participant C as Client
    participant U as User Service
    participant DB as Cosmos DB
    participant AZ as Azure Email

    C->>U: POST /v2/signup<br/>(email, password, name)

    Note over U: Step 1: Check existing user
    U->>DB: findOne({email})
    DB-->>U: User or null

    alt User exists & verified
        U-->>C: 400 "Email already registered"
    end

    alt User exists & unverified
        Note over U: Step 2a: Resend OTP
        U->>U: generate_otp() → "123456"
        U->>AZ: Send OTP email
        AZ-->>U: Email sent
        U->>DB: Update OTP & expiry
        U-->>C: 200 {token}
    end

    alt New user
        Note over U: Step 2b: Create user
        U->>U: generate_otp() → "654321"
        U->>AZ: Send OTP email
        AZ-->>U: Email sent
        U->>DB: insertOne (user with OTP)
        U-->>C: 200 {token}
    end

Step 1: Check Existing User

Code (Lines 83-103):

existing_user = users_collection.find_one({"email": email})

if existing_user:
    # User already exists
    if existing_user.get("verified", False):
        # Already verified → Cannot signup again
        raise HTTPException(status_code=400, detail="Email already registered")

    # Unverified → Resend OTP
    otp = generate_otp()  # New 6-digit OTP
    expiration_time = time.time() + 60  # 60 seconds

    if send_otp_email(email, otp):
        users_collection.update_one(
            {"email": email},
            {"$set": {"otp": otp, "otp_expiration": expiration_time}}
        )
        return {"token": create_jwt_token({"email": email, "otp_sent": True})}
    else:
        raise HTTPException(status_code=500, detail="Failed to send OTP")

Database Query:

db.users_multichatbot_v2.findOne({ email: "user@example.com" });

Possible Results:

  1. Null → New user, proceed to create
  2. User with verified: true → Error 400
  3. User with verified: false → Resend OTP

Step 2a: Resend OTP (Unverified User)

Scenario: User signed up but never verified, trying to signup again

Flow:

  1. Generate new OTP: generate_otp()"123456"
  2. Send email via Azure
  3. Update existing document with new OTP
  4. Return JWT token

Database Operation:

db.users_multichatbot_v2.updateOne(
  { email: "user@example.com" },
  {
    $set: {
      otp: "123456",
      otp_expiration: 1735214460, // Current timestamp + 60
    },
  }
);

Step 2b: Create New User

Code (Lines 105-131):

otp = generate_otp()  # "654321"
expiration_time = time.time() + 60  # Unix timestamp
current_date = datetime.now().strftime("%Y-%m-%d")

if send_otp_email(email, otp):
    users_collection.insert_one({
        "email": email,
        "password": password,  # ⚠️ PLAIN TEXT!
        "name": name,
        "otp": otp,
        "otp_expiration": expiration_time,
        "verified": False,
        "account_status": "paused",  # Not active yet
        "payment_status": "pending",
        "user_created_at(DATE)": current_date,
        "subscription_id": "sub_009",  # Free plan
        "subscription_date": current_date
    })

    return {"token": create_jwt_token({"email": email, "otp_sent": True})}

New User Document:

{
    "_id": ObjectId("65a1b2c3d4e5f6789abc"),
    "email": "newuser@example.com",
    "password": "mypassword123",  // ⚠️ PLAIN TEXT - SECURITY ISSUE
    "name": "New User",
    "otp": "654321",
    "otp_expiration": 1735214460,  // Unix timestamp (60 seconds from now)
    "verified": false,
    "account_status": "paused",    // Will be "active" after verification
    "payment_status": "pending",
    "user_created_at(DATE)": "2025-01-15",
    "subscription_id": "sub_009",  // Free plan
    "subscription_date": "2025-01-15",
    // Note: user_id NOT set yet (set during OTP verification)
}

⚠️ CRITICAL SECURITY ISSUE:
Passwords stored in plain text - same issue as Auth Service!


OTP Generation

Code (Lines 134-135):

def generate_otp():
    return ''.join(random.choices("0123456789", k=6))

Example Output: "123456", "987654", "000000", etc.

Characteristics:

  • Length: 6 digits
  • Character Set: 0-9
  • Total Combinations: 1,000,000 (weak but acceptable for 60-second expiry)

Security Note:
With 1 million combinations and 60-second expiry, brute force is difficult but not impossible. Consider:

  • Adding rate limiting (e.g., 3 attempts per minute)
  • Lockout after 5 failed attempts

OTP System

Email Sending (Azure Communication Services)

Code Location: /user-service/src/utils/email_utils.py

Configuration (Lines 47-49):

EMAIL_ENDPOINT = "https://mailing-sevice.india.communication.azure.com/"
EMAIL_ACCESS_KEY = "CgdEWi6fBCJv4c0EHOkq6ZUSML0VQSG49qXgEfrtlLmXY76HV7DeJQQJ99BBACULyCplbknJAAAAAZCSbc3T"
SENDER_EMAIL = "DoNotReply@machineagents.ai"

email_client = EmailClient(endpoint=EMAIL_ENDPOINT, credential=EMAIL_ACCESS_KEY)

⚠️ SECURITY ISSUE: Hardcoded credentials! Should use environment variables.


Send OTP Email Function

Code (Lines 64-79):

def send_otp_email(email: str, otp: str):
    try:
        message = {
            "senderAddress": SENDER_EMAIL,  # DoNotReply@machineagents.ai
            "recipients": {"to": [{"address": email}]},
            "content": {
                "subject": "Your OTP Code",
                "plainText": f"Your OTP is {otp}. It is valid for 60 seconds."
            }
        }
        email_client.begin_send(message)
        _log_json("SUCCESS", "OTP email sent successfully", email_recipient=email)
        return True
    except Exception as e:
        _log_json("ERROR", "Failed to send OTP email",
                 email_recipient=email,
                 exception_type=type(e).__name__,
                 exception_message=str(e),
                 stack_trace=traceback.format_exc())
        return False

Email Content

Subject: Your OTP Code

Body (Plain Text):

Your OTP is 123456. It is valid for 60 seconds.

Improvement Suggestion:

Welcome to MachineAvatars!

Your verification code is: 123456

This code will expire in 60 seconds.

If you did not request this code, please ignore this email.

---
MachineAvatars Team
https://machineavatars.com

OTP Expiration

Expiry Time: 60 seconds

Calculation (Line 92, 107):

expiration_time = time.time() + 60
# Example: 1735214400 + 60 = 1735214460

Validation (Line 147):

if time.time() > user.get("otp_expiration", 0):
    raise HTTPException(status_code=400, detail="OTP expired")

Example:

  • OTP Created: 14:00:00 (timestamp: 1735214400)
  • Expiry: 14:01:00 (timestamp: 1735214460)
  • User tries at 14:01:30: ❌ Expired!

Email Integration (Azure)

Azure Communication Services Setup

Service: Azure Communication Email
Region: India (india.communication.azure.com)
Sender Domain: machineagents.ai

Python Client:

from azure.communication.email import EmailClient

email_client = EmailClient(
    endpoint="https://mailing-sevice.india.communication.azure.com/",
    credential="CgdEWi6fBCJv4..."  # Access Key
)

Email Message Structure

Azure API Format:

message = {
    "senderAddress": "DoNotReply@machineagents.ai",
    "recipients": {
        "to": [
            {"address": "user@example.com"}
        ]
    },
    "content": {
        "subject": "Your OTP Code",
        "plainText": "Your OTP is 123456. It is valid for 60 seconds."
        # Could also include "html" for rich emails
    }
}

email_client.begin_send(message)

Email Delivery

Method: begin_send() - Asynchronous
Typical Delivery Time: 1-5 seconds
Retry Logic: None (returns True/False immediately)

Improvement:

poller = email_client.begin_send(message)
result = poller.result()  # Wait for confirmation

Logging

Success Log:

{
  "timestamp": "2025-01-15T14:00:00Z",
  "level": "SUCCESS",
  "message": "OTP email sent successfully",
  "service_name": "email-service",
  "email_recipient": "user@example.com"
}

Error Log:

{
  "timestamp": "2025-01-15T14:00:00Z",
  "level": "ERROR",
  "message": "Failed to send OTP email",
  "service_name": "email-service",
  "email_recipient": "user@example.com",
  "exception_type": "HttpResponseError",
  "exception_message": "Authentication failed",
  "stack_trace": "Traceback..."
}

OTP Verification Flow

Endpoint: POST /v2/verify-otp

Code (Lines 138-182):

@app.post("/v2/verify-otp")
async def verify_otp(email: str = Form(...), otp: str = Form(...)):
    user = users_collection.find_one({"email": email})

    if not user:
        raise HTTPException(status_code=404, detail="User not found")

    # Check expiration
    if time.time() > user.get("otp_expiration", 0):
        raise HTTPException(status_code=400, detail="OTP expired")

    # Validate OTP
    if user["otp"] == otp:
        # Generate or use existing user_id
        existing_user_id = user.get("user_id")
        if existing_user_id:
            user_id = existing_user_id
        else:
            user_id = f"User-{random.randint(100000, 999999)}"

        # Mark user as verified and active
        users_collection.update_one(
            {"email": email},
            {"$set": {
                "verified": True,
                "user_id": user_id,
                "account_status": "active"  # Activate account
            }}
        )

        # Assign free plan features
        await assign_free_plan_features(user_id)

        return {"message": "OTP verified successfully", "user_id": user_id}

    raise HTTPException(status_code=400, detail="Invalid OTP")

Flow Diagram

sequenceDiagram
    participant C as Client
    participant U as User Service
    participant DB as Cosmos DB

    C->>U: POST /v2/verify-otp<br/>(email, otp)

    U->>DB: findOne({email})
    DB-->>U: User document

    alt User not found
        U-->>C: 404 "User not found"
    end

    alt OTP expired
        Note over U: time.time() > otp_expiration
        U-->>C: 400 "OTP expired"
    end

    alt OTP invalid
        Note over U: user["otp"] != otp
        U-->>C: 400 "Invalid OTP"
    end

    alt OTP valid
        Note over U: Generate user_id if needed
        U->>DB: updateOne (verified=true, user_id, active)
        Note over U: Step 2: Assign features
        U->>U: assign_free_plan_features(user_id)
        U->>DB: Insert features_per_user
        U-->>C: 200 {message, user_id}
    end

User ID Generation

Code (Lines 154-159):

existing_user_id = user.get("user_id")
if existing_user_id:
    user_id = existing_user_id  # Already has ID (edge case)
else:
    user_id = f"User-{random.randint(100000, 999999)}"
    # Example: "User-123456", "User-987654"

Format: User-XXXXXX (6 random digits)

Collision Probability: Low but possible (1 in 1 million)

Better Approach:

import time
user_id = f"User-{int(time.time())}_{random.randint(100, 999)}"
# Example: "User-1735214400_456"

Account Activation

Database Update:

db.users_multichatbot_v2.updateOne(
  { email: "user@example.com" },
  {
    $set: {
      verified: true,
      user_id: "User-123456",
      account_status: "active", // Was "paused" during signup
    },
  }
);

Before:

{
    "email": "user@example.com",
    "verified": false,
    "account_status": "paused",
    "user_id": undefined  // Not set
}

After:

{
    "email": "user@example.com",
    "verified": true,
    "account_status": "active",
    "user_id": "User-123456"
}

Feature Assignment System

Free Plan Assignment

Code (Lines 185-237):

Trigger: After successful OTP verification

Function: assign_free_plan_features(user_id)


Step-by-Step Process

1. Get Subscription Details

subscription_id = "sub_009"  # Free plan ID
subscription = db["subscriptions"].findOne({"subscription_id": subscription_id})

Subscription Document:

{
    "_id": ObjectId("..."),
    "subscription_id": "sub_009",
    "name": "Free Plan",
    "pricing": 0,
    "offers": "Basic features for trying out",
    "featureIDs": ["feat_001", "feat_002"],  // Feature IDs included
    "no_of_chatbots": 1,
    "no_of_chat_sessions": 50,
    "no_of_linkcrawls": "50",
    "no_of_users": "1",
    "grace_period": "14"
}

2. Get Base Features

base_features = db["baseFeatures"].findOne({"subscription_id": subscription_id})

Base Features Document:

{
    "_id": ObjectId("..."),
    "subscription_id": "sub_009",
    "baseFeatureID": "base_009",
    "no_of_chatbots": 1,
    "no_of_chat_sessions": 50,
    "no_of_linkcrawls": "50",
    "no_of_users": "1",
    "grace_period": "14"
}

3. Parse Feature IDs

Code (Lines 205-215):

feature_ids = subscription.get("featureIDs", [])

# Handle both list and comma-separated string formats
if not isinstance(feature_ids, list):
    if isinstance(feature_ids, str) and feature_ids:
        feature_ids = [fid.strip() for fid in feature_ids.split(",") if fid.strip()]
    else:
        feature_ids = []

Examples:

  • List: ["feat_001", "feat_002"]["feat_001", "feat_002"]
  • String: "feat_001, feat_002"["feat_001", "feat_002"]
  • Empty: ""[]

4. Create User Features Record

Code (Lines 218-231):

user_features = {
    "user_id": user_id,
    "subscription_id": subscription_id,
    "baseFeatureID": base_features.get("baseFeatureID", "base_009"),
    "featureIDs": feature_ids,
    "no_of_chatbots": base_features.get("no_of_chatbots", 1),
    "no_of_chat_sessions": base_features.get("no_of_chat_sessions", 50),
    "no_of_linkcrawls": base_features.get("no_of_linkcrawls", "50"),
    "no_of_users": base_features.get("no_of_users", "1"),
    "grace_period": base_features.get("grace_period", "14")
}

features_per_user_collection.insert_one(user_features)

Inserted Document:

// Collection: features_per_user
{
    "_id": ObjectId("..."),
    "user_id": "User-123456",
    "subscription_id": "sub_009",
    "baseFeatureID": "base_009",
    "featureIDs": ["feat_001", "feat_002"],
    "no_of_chatbots": 1,        // Can create 1 chatbot
    "no_of_chat_sessions": 50,   // 50 conversations/month
    "no_of_linkcrawls": "50",    // Can crawl 50 URLs
    "no_of_users": "1",          // Single user (no team)
    "grace_period": "14"         // 14 days grace period
}

Free Plan Limits

Limit Value Description
Chatbots 1 Can create 1 chatbot
Chat Sessions 50 50 conversations per month
Link Crawls 50 Can crawl 50 web pages
Users 1 Single user (no team members)
Grace Period 14 days Time before account restrictions

Password Reset Flow

Step 1: Forgot Password

Endpoint: POST /v2/forgot-password
Code (Lines 241-260):

@app.post("/v2/forgot-password")
async def forgot_password(email: str = Form(...)):
    user = users_collection.find_one({"email": email})
    if not user:
        raise HTTPException(status_code=404, detail="Email not registered")

    otp = generate_otp()
    expiration_time = time.time() + 60

    if send_otp_email(email, otp):
users_collection.update_one(
            {"email": email},
            {"$set": {"otp": otp, "otp_expiration": expiration_time}}
        )
        return {"token": create_jwt_token({"email": email, "otp_sent": True})}
    else:
        raise HTTPException(status_code=500, detail="Failed to send OTP")

Flow:

  1. Check if email exists
  2. Generate new OTP (60-second expiry)
  3. Send OTP via email
  4. Update user document with OTP
  5. Return JWT token

Step 2: Reset Password

Endpoint: POST /v2/reset-password
Code (Lines 263-283):

@app.post("/v2/reset-password")
async def reset_password(
    email: str = Form(...),
    otp: str = Form(...),
    new_password: str = Form(...)
):
    user = users_collection.find_one({"email": email})
    if not user:
        raise HTTPException(status_code=404, detail="User not found")

    if time.time() > user.get("otp_expiration", 0):
        raise HTTPException(status_code=400, detail="OTP expired")

    if user["otp"] == otp:
        # Update password (PLAIN TEXT!)
        users_collection.update_one(
            {"email": email},
            {"$set": {"password": new_password}}
        )
        return {"token": create_jwt_token({"email": email, "password_reset": True})}

    raise HTTPException(status_code=400, detail="Invalid OTP")

⚠️ SECURITY ISSUE: Password updated as plain text (Line 278)!


Complete Reset Flow

sequenceDiagram
    participant C as Client
    participant U as User Service
    participant DB as Cosmos DB
    participant AZ as Azure Email

    Note over C,AZ: STEP 1: Forgot Password
    C->>U: POST /v2/forgot-password<br/>(email)
    U->>DB: findOne({email})
    DB-->>U: User found
    U->>U: generate_otp()
    U->>AZ: Send OTP email
    AZ-->>U: Email sent
    U->>DB: Update OTP
    U-->>C: {token}

    Note over C: User checks email, gets OTP

    Note over C,AZ: STEP 2: Reset Password
    C->>U: POST /v2/reset-password<br/>(email, otp, new_password)
    U->>DB: findOne({email})
    DB-->>U: User with OTP

    alt OTP valid
        U->>DB: updateOne (password = new_password)
        U-->>C: {token}
    else OTP invalid/expired
        U-->>C: 400 Error
    end

Get User Features

Endpoint: GET /v2/get-user-features/{user_id}

Code (Lines 286-422):

Purpose: Retrieve complete feature set for a user by aggregating 4 collections


Collections Queried

  1. features_per_user - User's assigned features
  2. subscriptions - Subscription details (pricing, offers)
  3. baseFeatures - Base limits for subscription
  4. featuresGlobal - Individual feature details

Data Aggregation Flow

graph TB
    A[GET /v2/get-user-features/User-123456] --> B[Query features_per_user]
    B --> C{Found?}
    C -->|No| D[Return 404]
    C -->|Yes| E[Get subscription_id]
    E --> F[Query subscriptions collection]
    F --> G{Found in subscriptions?}
    G -->|No| H[Query roles collection<br/>Fallback]
    H --> I[Convert role to subscription format]
    G -->|Yes| J[Get baseFeatureID]
    I --> J
    J --> K[Query baseFeatures]
    K --> L[Get featureIDs]
    L --> M[Query featuresGlobal<br/>Using $in operator]
    M --> N[Aggregate all data]
    N --> O[Return merged response]

Response Structure

{
  "user_features": {
    "user_id": "User-123456",
    "subscription_id": "sub_009",
    "baseFeatureID": "base_009",
    "featureIDs": ["feat_001", "feat_002"],

    // User limits (priority: user > base > subscription)
    "no_of_chatbots": 1,
    "no_of_chat_sessions": 50,
    "no_of_linkcrawls": "50",
    "no_of_users": "1",
    "grace_period": "14",

    // Subscription details
    "subscription_details": {
      "pricing": 0,
      "offers": "Basic features for trying out"
    },

    // Base feature details
    "base_feature_details": {
      "no_of_chatbots": 1,
      "no_of_chat_sessions": 50,
      "no_of_users": "1",
      "no_of_linkcrawls": "50",
      "grace_period": "14"
    },

    // Individual features
    "features": [
      {
        "featureID": "feat_001",
        "feature": "3D Avatar Support",
        "feature_value": "true"
      },
      {
        "featureID": "feat_002",
        "feature": "Basic Analytics",
        "feature_value": "enabled"
      }
    ]
  }
}

Priority System

Code (Lines 389-393):

"no_of_chatbots": (
    user_features.get("no_of_chatbots") or
    base_feature.get("no_of_chatbots") or
    subscription.get("no_of_chatbots", "1")
)

Priority Order:

  1. User-specific override (features_per_user)
  2. Base features (baseFeatures collection)
  3. Subscription default (subscriptions collection)
  4. Hardcoded fallback ("1")

Database Schemas

1. users_multichatbot_v2

Complete Schema:

{
    _id: ObjectId("..."),

    // Identity
    email: "user@example.com",  // Unique, indexed
    user_id: "User-123456",     // Generated during OTP verification
    name: "John Doe",

    // Authentication
    password: "plaintext123",   // ⚠️ PLAIN TEXT!
    verified: true,             // Email verified via OTP

    // OTP System
    otp: "123456",              // Current OTP (or last used)
    otp_expiration: 1735214460, // Unix timestamp (60 seconds)

    // Account Status
    account_status: "active",   // "paused" or "active"
    payment_status: "pending",  // "pending", "paid", "failed"

    // Subscription
    subscription_id: "sub_009",       // Free plan
    subscription_date: "2025-01-15",

    // Timestamps
    user_created_at(DATE): "2025-01-15",  // Note: Unusual field name with ()
    last_login: "2025-01-20T14:00:00Z"    // Optional
}

2. features_per_user

Schema:

{
    _id: ObjectId("..."),
    user_id: "User-123456",
    subscription_id: "sub_009",
    baseFeatureID: "base_009",
    featureIDs: ["feat_001", "feat_002"],

    // Limits
    no_of_chatbots: 1,
    no_of_chat_sessions: 50,
    no_of_linkcrawls: "50",  // Note: String
    no_of_users: "1",        // Note: String
    grace_period: "14"       // Days
}

3. subscriptions

Schema:

{
    _id: ObjectId("..."),
    subscription_id: "sub_009",
    name: "Free Plan",
    pricing: 0,
    offers: "Basic features for trying out",
    baseFeatureID: "base_009",
    featureIDs: ["feat_001", "feat_002"],  // Can be string: "feat_001,feat_002"

    // Limits
    no_of_chatbots: 1,
    no_of_chat_sessions: 50,
    no_of_linkcrawls: "50",
    no_of_users: "1",
    grace_period: "14"
}

4. baseFeatures

Schema:

{
    _id: ObjectId("..."),
    subscription_id: "sub_009",
    baseFeatureID: "base_009",

    // Limits
    no_of_chatbots: 1,
    no_of_chat_sessions: 50,
    no_of_linkcrawls: "50",
    no_of_users: "1",
    grace_period: "14"
}

5. featuresGlobal

Schema:

{
    _id: ObjectId("..."),
    featureID: "feat_001",
    feature: "3D Avatar Support",
    feature_value: "true",  // or "enabled", numeric, etc.
    description: "Allows users to use 3D avatars in chatbots"
}

6. roles (Fallback)

Schema (Lines 327-340):

{
    _id: ObjectId("..."),
    role_id: "role_admin",  // Used as subscription_id fallback
    baseFeatureID: "base_admin",
    featureIDs: [...],
    no_of_chatbots: 999,
    no_of_chat_sessions: 999999,
    // ... etc
}

Note: Roles collection used as fallback if subscription not found (Lines 324-343)


Security Analysis

Critical Issues

1. ⚠️ PLAIN TEXT PASSWORDS

Locations:

  • Line 115: insertOne({"password": password})
  • Line 278: updateOne({"$set": {"password": new_password}})

Impact:

  • Same as Auth Service - all passwords exposed if DB breached
  • Violates GDPR, OWASP, PCI-DSS

Fix:

import bcrypt

# Signup
password_hash = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt(rounds=12))
users_collection.insert_one({"password_hash": password_hash.decode()})

# Reset
new_hash = bcrypt.hashpw(new_password.encode('utf-8'), bcrypt.gensalt(rounds=12))
users_collection.update_one({"email": email}, {"$set": {"password_hash": new_hash.decode()}})

2. ⚠️ HARDCODED EMAIL CREDENTIALS

Location: /user-service/src/utils/email_utils.py (Lines 47-48)

Problem:

EMAIL_ENDPOINT = "https://mailing-sevice.india.communication.azure.com/"
EMAIL_ACCESS_KEY = "CgdEWi6fBCJv4..."  # Hardcoded!

Fix:

EMAIL_ENDPOINT = os.getenv("AZURE_EMAIL_ENDPOINT")
EMAIL_ACCESS_KEY = os.getenv("AZURE_EMAIL_ACCESS_KEY")
SENDER_EMAIL = os.getenv("SENDER_EMAIL", "DoNotReply@machineagents.ai")

if not EMAIL_ENDPOINT or not EMAIL_ACCESS_KEY:
    raise ValueError("Email credentials must be set in environment")

3. ⚠️ NO RATE LIMITING

Problem: Unlimited OTP requests possible

Attack Scenarios:

  1. Email bombing: Request OTP repeatedly to spam user's inbox
  2. Cost attack: Generate Azure email costs
  3. Brute force: Try all 1 million OTP combinations (unlikely but possible)

Fix:

from slowapi import Limiter

limiter = Limiter(key_func=get_remote_address)

@app.post("/v2/signup")
@limiter.limit("3/minute")  # 3 signups per minute
async def signup(...):
    ...

@app.post("/v2/verify-otp")
@limiter.limit("5/minute")  # 5 OTP attempts per minute
async def verify_otp(...):
    ...

4. ⚠️ NO ACCOUNT LOCKOUT

Problem: No protection after multiple failed OTP attempts

Fix:

// Add to user document
{
    "failed_otp_attempts": 0,
    "account_locked_until": null
}

Code:

if user.get("failed_otp_attempts", 0) >= 5:
    if user.get("account_locked_until", 0) > time.time():
        raise HTTPException(status_code=403, detail="Account temporarily locked")

if user["otp"] != otp:
    users_collection.update_one(
        {"email": email},
        {"$inc": {"failed_otp_attempts": 1}}
    )
    if user.get("failed_otp_attempts", 0) + 1 >= 5:
        users_collection.update_one(
            {"email": email},
            {"$set": {"account_locked_until": time.time() + 900}}  # 15 min lockout
        )
    raise HTTPException(status_code=400, detail="Invalid OTP")

5. ⚠️ PREDICTABLE USER IDs

Problem (Line 158):

user_id = f"User-{random.randint(100000, 999999)}"
# Only 1 million possibilities, collisions possible

Fix:

import uuid
user_id = f"User-{uuid.uuid4().hex[:12]}"
# Example: "User-a1b2c3d4e5f6"

6. ⚠️ CORS ALLOWS ALL ORIGINS

Location: Lines 24-30

Same as Auth Service - should restrict to specific domains


Security Best Practices Implemented

OTP Expiration: 60-second time limit
Email Verification: Required before account activation
HTTPS: Enforced in production
Logging: Comprehensive JSON logging
Error Messages: Generic (though could be better)


Deployment

Docker Configuration

Dockerfile:

FROM python:3.9-slim

WORKDIR /app

# Install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy source code
COPY src/ .

EXPOSE 8002

CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8002"]

Docker Compose

user-service:
  build: ./user-service
  container_name: user-service
  ports:
    - "8002:8002"
  networks:
    - app-network
  labels:
    - "logging=loki"
    - "app=machine-agent-app"
  environment:
    - MONGO_URI=${MONGO_URI}
    - MONGO_DB_NAME=Machine_agent_demo
    - JWT_SECRET=${JWT_SECRET}
    - JWT_ALGORITHM=HS256

    # Azure Email (should be added)
    - AZURE_EMAIL_ENDPOINT=${AZURE_EMAIL_ENDPOINT}
    - AZURE_EMAIL_ACCESS_KEY=${AZURE_EMAIL_ACCESS_KEY}

    # DataDog APM
    - DD_SERVICE=user-service
    - DD_ENV=production
    - DD_VERSION=1.0.0
    - DD_TRACE_ENABLED=true
  restart: always

Requirements.txt

# Web framework
fastapi>=0.95.0
uvicorn[standard]>=0.22.0

# Database
pymongo>=4.3.3

# Authentication & Security
PyJWT>=2.8.0
python-jose>=3.3.0
python-multipart>=0.0.6

# Environment variables
python-dotenv>=1.0.0

# Azure Email
azure-communication-email>=1.0.0

# Monitoring
ddtrace>=1.19.0

Performance Metrics

Operation Latency Notes
Signup (without email) 50-100ms MongoDB insert
Signup (with email) 2-5 seconds Azure email API
Verify OTP 100-200ms MongoDB + feature assignment
Forgot Password 2-5 seconds MongoDB + email
Reset Password 50-100ms MongoDB update
Get User Features 150-300ms 4 collection queries + aggregation

Bottleneck: Azure email sending (2-5 seconds)



Recommendations & Next Steps

Critical (Security)

  1. ⚠️ IMPLEMENT PASSWORD HASHING (bcrypt) - URGENT!
  2. ⚠️ Move Email Credentials to Environment Variables
  3. ⚠️ Add Rate Limiting (slowapi)
  4. ⚠️ Improve User ID Generation (UUID-based)

Improvements

  1. Add Account Lockout - After 5 failed OTP attempts
  2. Enhance Email Template - Branded HTML emails
  3. Add Email Retry Logic - Handle Azure failures
  4. Improve Error Messages - More user-friendly

Code Quality

  1. Fix Field Name: user_created_at(DATE)created_at
  2. Consistent Types: no_of_* should be integers, not strings
  3. Add Unit Tests - Test OTP generation, expiration, etc.
  4. Add Input Validation - Email format, password strength

Last Updated: 2025-12-26
Code Version: user-service/src/main.py (444 lines)
Total Endpoints: 5
Review Cycle: Monthly (Critical Security Service)


"Where users become users - one OTP at a time."