File size: 7,150 Bytes
eda187d
65c84d0
eda187d
65c84d0
 
 
 
 
eda187d
 
65c84d0
eda187d
 
 
65c84d0
 
eda187d
65c84d0
 
 
eda187d
65c84d0
 
 
 
 
eda187d
 
65c84d0
 
 
 
 
 
eda187d
 
 
65c84d0
eda187d
 
65c84d0
 
 
eda187d
 
65c84d0
 
 
eda187d
 
65c84d0
 
 
eda187d
65c84d0
 
eda187d
65c84d0
 
 
 
 
eda187d
65c84d0
 
 
 
 
 
 
 
eda187d
65c84d0
 
 
eda187d
65c84d0
 
 
eda187d
65c84d0
 
eda187d
65c84d0
 
eda187d
65c84d0
 
eda187d
65c84d0
 
 
 
 
 
 
 
 
eda187d
 
 
65c84d0
eda187d
 
65c84d0
 
 
 
eda187d
 
65c84d0
 
 
 
eda187d
 
 
65c84d0
eda187d
 
65c84d0
 
 
 
eda187d
 
65c84d0
 
 
eda187d
65c84d0
 
 
 
 
 
eda187d
 
65c84d0
 
eda187d
65c84d0
 
 
 
 
eda187d
 
65c84d0
 
 
 
eda187d
 
65c84d0
 
 
eda187d
65c84d0
 
eda187d
65c84d0
 
 
 
eda187d
65c84d0
 
 
 
 
 
 
 
eda187d
65c84d0
 
eda187d
65c84d0
eda187d
65c84d0
 
 
 
 
eda187d
65c84d0
 
 
 
 
eda187d
65c84d0
 
 
 
 
 
 
eda187d
65c84d0
eda187d
 
65c84d0
 
 
eda187d
65c84d0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
"""
OmniParse AI - Authentication Module

Handles user registration, login, session management, and API key generation.
Uses bcrypt for password hashing and delegates all storage to database.py.

Late-imports database functions inside call-bodies to avoid circular imports
(auth ↔ database).
"""

import secrets

import bcrypt

from validation import validate_email, validate_name, validate_password, sanitize_string


# ---------------------------------------------------------------------------
# Password helpers
# ---------------------------------------------------------------------------

def hash_pw(password: str) -> str:
    """Hash *password* with bcrypt (12 rounds) and return the hash as a UTF-8 string."""
    salt = bcrypt.gensalt(rounds=12)
    hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
    return hashed.decode("utf-8")


def verify_pw(password: str, hashed: str) -> bool:
    """Return True if *password* matches the bcrypt *hashed* string."""
    try:
        return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
    except Exception:
        return False


# ---------------------------------------------------------------------------
# Token / API-key generators
# ---------------------------------------------------------------------------

def gen_token() -> str:
    """Generate a cryptographically random session token (URL-safe, 32 bytes)."""
    return secrets.token_urlsafe(32)


def gen_api_key() -> str:
    """Generate a unique API key with a recognizable prefix."""
    return "op_live_" + secrets.token_hex(20)


# ---------------------------------------------------------------------------
# User creation
# ---------------------------------------------------------------------------

def create_user(email: str, name: str, password: str, plan: str = "free"):
    """Register a new user after validating all inputs.

    Args:
        email: User email address.
        name: Display name.
        password: Plain-text password (will be hashed).
        plan: Subscription plan (default "free").

    Returns:
        (user_dict, "") on success.
        (None, "error message") on failure.
    """
    # --- Input validation ---
    valid, err = validate_email(email)
    if not valid:
        return None, err

    valid, err = validate_name(name)
    if not valid:
        return None, err

    valid, errs = validate_password(password)
    if not valid:
        return None, "; ".join(errs)

    email = sanitize_string(email).lower()
    name = sanitize_string(name)

    if plan not in ("free", "basic", "pro", "enterprise"):
        return None, "Invalid plan."

    # --- Late import to avoid circular dependency ---
    from database import get_user_by_email, create_user as db_create_user

    # --- Check uniqueness ---
    existing = get_user_by_email(email)
    if existing is not None:
        return None, "An account with this email already exists."

    # --- Hash & store ---
    hashed = hash_pw(password)
    user = db_create_user(email=email, name=name, password=hashed, plan=plan)
    return user, ""


# ---------------------------------------------------------------------------
# User lookups
# ---------------------------------------------------------------------------

def get_user_by_email(email: str):
    """Fetch a user by email. Returns user dict or None."""
    from database import get_user_by_email as db_get_user_by_email
    return db_get_user_by_email(email)


def get_user_by_id(uid: str):
    """Fetch a user by ID. Returns user dict or None."""
    from database import get_user_by_id as db_get_user_by_id
    return db_get_user_by_id(uid)


# ---------------------------------------------------------------------------
# User update
# ---------------------------------------------------------------------------

def update_user(uid: str, fields: dict):
    """Update user fields. Returns the updated user dict or None on failure."""
    from database import update_user as db_update_user
    return db_update_user(uid, fields)


# ---------------------------------------------------------------------------
# Session management
# ---------------------------------------------------------------------------

def create_session(user_id: str) -> str:
    """Create a new session for *user_id* and return the session token."""
    from database import create_session as db_create_session
    token = gen_token()
    db_create_session(user_id=user_id, token=token)
    return token


def get_session_user(token: str):
    """Look up the user associated with *token*.

    Returns:
        user_dict if the session is valid and not expired, else None.
    """
    from database import get_session_user as db_get_session_user
    return db_get_session_user(token)


def delete_session(token: str) -> None:
    """Delete a session token (logout)."""
    from database import delete_session as db_delete_session
    db_delete_session(token)


# ---------------------------------------------------------------------------
# Authentication (login)
# ---------------------------------------------------------------------------

def authenticate_user(email: str, password: str, ip: str = ""):
    """Verify credentials (session is created separately by the caller).

    Args:
        email: User email.
        password: Plain-text password.
        ip: Client IP address for rate-limiting.

    Returns:
        (user_dict, "") on success.
        (None, "error message") on failure.
    """
    # --- Input validation ---
    valid, err = validate_email(email)
    if not valid:
        return None, err

    if not isinstance(password, str) or not password:
        return None, "Password is required."

    email = sanitize_string(email).lower()

    # --- Rate limit check ---
    if ip:
        from rate_limiter import check_auth_rate
        if not check_auth_rate(ip):
            return None, "Too many login attempts. Please try again later."

    # --- Look up user ---
    from database import get_user_by_email as db_get_user_by_email
    user = db_get_user_by_email(email)
    if user is None:
        return None, "Invalid email or password."

    # --- Verify password ---
    stored_hash = user.get("password", "")
    if not stored_hash:
        return None, "Invalid email or password."

    if not verify_pw(password, stored_hash):
        return None, "Invalid email or password."

    return user, ""


# ---------------------------------------------------------------------------
# Demo account bootstrap
# ---------------------------------------------------------------------------

def ensure_demo_account():
    """Create a demo account if one does not already exist."""
    from database import get_user_by_email, create_user as db_create_user
    existing = get_user_by_email("demo@omniparse.ai")
    if existing is not None:
        return
    try:
        hashed = hash_pw("demo1234")
        db_create_user(
            email="demo@omniparse.ai",
            name="Demo User",
            password=hashed,
            plan="pro",
        )
    except Exception as e:
        print(f"[INFO] Demo account not created: {e}")