KillerKing93 commited on
Commit
97e0d9c
·
verified ·
1 Parent(s): 37a245b

Sync from GitHub f4738e2

Browse files
Files changed (7) hide show
  1. .env.example +6 -0
  2. auth.py +313 -0
  3. docker-compose.yml +113 -0
  4. main.py +137 -0
  5. models.py +3 -0
  6. requirements.txt +4 -0
  7. tests/test_api.py +41 -22
.env.example CHANGED
@@ -7,6 +7,12 @@ DATABASE_URL=sqlite:///./marketplace.db
7
  # PostgreSQL example: postgresql://user:password@localhost/marketplace
8
  # MySQL example: mysql+pymysql://user:password@localhost/marketplace
9
 
 
 
 
 
 
 
10
  # Model from Hugging Face (Transformers)
11
  MODEL_REPO_ID=unsloth/Qwen3-4B-Instruct-2507
12
  # HF token for gated/private models (optional)
 
7
  # PostgreSQL example: postgresql://user:password@localhost/marketplace
8
  # MySQL example: mysql+pymysql://user:password@localhost/marketplace
9
 
10
+ # Authentication (JWT)
11
+ # IMPORTANT: Change JWT_SECRET_KEY in production! Use a strong random secret.
12
+ JWT_SECRET_KEY=your-super-secret-jwt-key-change-in-production
13
+ ACCESS_TOKEN_EXPIRE_MINUTES=30
14
+ REFRESH_TOKEN_EXPIRE_DAYS=7
15
+
16
  # Model from Hugging Face (Transformers)
17
  MODEL_REPO_ID=unsloth/Qwen3-4B-Instruct-2507
18
  # HF token for gated/private models (optional)
auth.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Authentication and authorization for AI Marketplace Platform
5
+
6
+ Features:
7
+ - JWT token generation and validation
8
+ - Password hashing with bcrypt
9
+ - Role-based access control (user, supplier, admin)
10
+ - Token refresh mechanism
11
+ - FastAPI dependencies for protected endpoints
12
+
13
+ Usage:
14
+ from auth import get_current_user, create_access_token
15
+
16
+ @app.post("/protected")
17
+ def protected_route(current_user: dict = Depends(get_current_user)):
18
+ return {"user": current_user}
19
+ """
20
+
21
+ import os
22
+ from datetime import datetime, timedelta
23
+ from typing import Optional, Dict, Any
24
+ from passlib.context import CryptContext
25
+ from jose import JWTError, jwt
26
+ from fastapi import Depends, HTTPException, status
27
+ from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
28
+ from pydantic import BaseModel, EmailStr
29
+
30
+ # JWT Configuration
31
+ SECRET_KEY = os.getenv("JWT_SECRET_KEY", "your-secret-key-change-in-production")
32
+ ALGORITHM = "HS256"
33
+ ACCESS_TOKEN_EXPIRE_MINUTES = int(os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "30"))
34
+ REFRESH_TOKEN_EXPIRE_DAYS = int(os.getenv("REFRESH_TOKEN_EXPIRE_DAYS", "7"))
35
+
36
+ # Password hashing
37
+ pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
38
+
39
+ # Security scheme
40
+ security = HTTPBearer()
41
+
42
+
43
+ # Pydantic models
44
+ class Token(BaseModel):
45
+ access_token: str
46
+ refresh_token: str
47
+ token_type: str = "bearer"
48
+
49
+
50
+ class TokenData(BaseModel):
51
+ email: Optional[str] = None
52
+ role: Optional[str] = None
53
+
54
+
55
+ class UserLogin(BaseModel):
56
+ email: EmailStr
57
+ password: str
58
+
59
+
60
+ class UserRegisterAuth(BaseModel):
61
+ email: EmailStr
62
+ password: str
63
+ name: str
64
+ role: str = "user" # user, supplier, admin
65
+
66
+
67
+ # Password utilities
68
+ def hash_password(password: str) -> str:
69
+ """Hash a password for storing."""
70
+ return pwd_context.hash(password)
71
+
72
+
73
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
74
+ """Verify a stored password against one provided by user."""
75
+ return pwd_context.verify(plain_password, hashed_password)
76
+
77
+
78
+ # Token utilities
79
+ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
80
+ """
81
+ Create JWT access token.
82
+
83
+ Args:
84
+ data: Dictionary with user data (email, role, etc.)
85
+ expires_delta: Optional token expiration time
86
+
87
+ Returns:
88
+ Encoded JWT token string
89
+ """
90
+ to_encode = data.copy()
91
+ if expires_delta:
92
+ expire = datetime.utcnow() + expires_delta
93
+ else:
94
+ expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
95
+
96
+ to_encode.update({"exp": expire})
97
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
98
+ return encoded_jwt
99
+
100
+
101
+ def create_refresh_token(data: dict) -> str:
102
+ """
103
+ Create JWT refresh token with longer expiration.
104
+
105
+ Args:
106
+ data: Dictionary with user data
107
+
108
+ Returns:
109
+ Encoded JWT refresh token string
110
+ """
111
+ to_encode = data.copy()
112
+ expire = datetime.utcnow() + timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
113
+ to_encode.update({"exp": expire, "type": "refresh"})
114
+ encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
115
+ return encoded_jwt
116
+
117
+
118
+ def verify_token(token: str) -> Dict[str, Any]:
119
+ """
120
+ Verify and decode JWT token.
121
+
122
+ Args:
123
+ token: JWT token string
124
+
125
+ Returns:
126
+ Decoded token payload
127
+
128
+ Raises:
129
+ HTTPException: If token is invalid or expired
130
+ """
131
+ try:
132
+ payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
133
+ email: str = payload.get("email")
134
+ if email is None:
135
+ raise HTTPException(
136
+ status_code=status.HTTP_401_UNAUTHORIZED,
137
+ detail="Could not validate credentials",
138
+ headers={"WWW-Authenticate": "Bearer"},
139
+ )
140
+ return payload
141
+ except JWTError:
142
+ raise HTTPException(
143
+ status_code=status.HTTP_401_UNAUTHORIZED,
144
+ detail="Could not validate credentials",
145
+ headers={"WWW-Authenticate": "Bearer"},
146
+ )
147
+
148
+
149
+ # FastAPI dependencies
150
+ async def get_current_user(
151
+ credentials: HTTPAuthorizationCredentials = Depends(security)
152
+ ) -> Dict[str, Any]:
153
+ """
154
+ FastAPI dependency to get current authenticated user.
155
+
156
+ Usage:
157
+ @app.get("/protected")
158
+ def protected_route(current_user: dict = Depends(get_current_user)):
159
+ return {"user": current_user}
160
+
161
+ Returns:
162
+ User data from token payload
163
+
164
+ Raises:
165
+ HTTPException: If token is invalid
166
+ """
167
+ token = credentials.credentials
168
+ payload = verify_token(token)
169
+ return payload
170
+
171
+
172
+ async def get_current_active_user(
173
+ current_user: dict = Depends(get_current_user)
174
+ ) -> Dict[str, Any]:
175
+ """
176
+ Get current active user (additional checks can be added here).
177
+
178
+ Args:
179
+ current_user: User from token
180
+
181
+ Returns:
182
+ User data if active
183
+
184
+ Raises:
185
+ HTTPException: If user is inactive
186
+ """
187
+ # Add additional checks here (e.g., is_active flag from database)
188
+ return current_user
189
+
190
+
191
+ async def require_role(required_role: str):
192
+ """
193
+ Dependency factory for role-based access control.
194
+
195
+ Usage:
196
+ @app.post("/admin/users", dependencies=[Depends(require_role("admin"))])
197
+ def admin_only_route():
198
+ return {"message": "Admin access"}
199
+
200
+ Args:
201
+ required_role: Required role (user, supplier, admin)
202
+
203
+ Returns:
204
+ Dependency function
205
+ """
206
+ async def role_checker(current_user: dict = Depends(get_current_user)):
207
+ user_role = current_user.get("role", "user")
208
+ if user_role != required_role and user_role != "admin":
209
+ raise HTTPException(
210
+ status_code=status.HTTP_403_FORBIDDEN,
211
+ detail=f"Insufficient permissions. Required role: {required_role}"
212
+ )
213
+ return current_user
214
+
215
+ return role_checker
216
+
217
+
218
+ # Helper for protected endpoints
219
+ def require_admin(current_user: dict = Depends(get_current_user)):
220
+ """Require admin role for endpoint."""
221
+ if current_user.get("role") != "admin":
222
+ raise HTTPException(
223
+ status_code=status.HTTP_403_FORBIDDEN,
224
+ detail="Admin access required"
225
+ )
226
+ return current_user
227
+
228
+
229
+ def require_supplier(current_user: dict = Depends(get_current_user)):
230
+ """Require supplier role for endpoint."""
231
+ user_role = current_user.get("role")
232
+ if user_role not in ["supplier", "admin"]:
233
+ raise HTTPException(
234
+ status_code=status.HTTP_403_FORBIDDEN,
235
+ detail="Supplier access required"
236
+ )
237
+ return current_user
238
+
239
+
240
+ # Utility functions for user authentication
241
+ def authenticate_user(email: str, password: str, db_user: Dict[str, Any]) -> bool:
242
+ """
243
+ Authenticate user with email and password.
244
+
245
+ Args:
246
+ email: User email
247
+ password: Plain password
248
+ db_user: User data from database (must include 'hashed_password' key)
249
+
250
+ Returns:
251
+ True if authentication successful, False otherwise
252
+ """
253
+ if not db_user:
254
+ return False
255
+ if not verify_password(password, db_user.get("hashed_password", "")):
256
+ return False
257
+ return True
258
+
259
+
260
+ def create_tokens_for_user(email: str, role: str = "user", **extra_data) -> Token:
261
+ """
262
+ Create access and refresh tokens for user.
263
+
264
+ Args:
265
+ email: User email
266
+ role: User role (user, supplier, admin)
267
+ **extra_data: Additional data to include in token
268
+
269
+ Returns:
270
+ Token object with access_token, refresh_token, and token_type
271
+ """
272
+ token_data = {"email": email, "role": role, **extra_data}
273
+
274
+ access_token = create_access_token(token_data)
275
+ refresh_token = create_refresh_token(token_data)
276
+
277
+ return Token(
278
+ access_token=access_token,
279
+ refresh_token=refresh_token,
280
+ token_type="bearer"
281
+ )
282
+
283
+
284
+ # Example: Refresh token endpoint logic
285
+ def refresh_access_token(refresh_token: str) -> str:
286
+ """
287
+ Generate new access token from refresh token.
288
+
289
+ Args:
290
+ refresh_token: Valid refresh token
291
+
292
+ Returns:
293
+ New access token
294
+
295
+ Raises:
296
+ HTTPException: If refresh token is invalid or not a refresh type
297
+ """
298
+ payload = verify_token(refresh_token)
299
+
300
+ # Check if it's a refresh token
301
+ if payload.get("type") != "refresh":
302
+ raise HTTPException(
303
+ status_code=status.HTTP_401_UNAUTHORIZED,
304
+ detail="Invalid token type"
305
+ )
306
+
307
+ # Create new access token
308
+ token_data = {
309
+ "email": payload.get("email"),
310
+ "role": payload.get("role")
311
+ }
312
+
313
+ return create_access_token(token_data)
docker-compose.yml ADDED
@@ -0,0 +1,113 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ version: '3.8'
2
+
3
+ services:
4
+ # PostgreSQL database
5
+ postgres:
6
+ image: postgres:15-alpine
7
+ container_name: marketplace-db
8
+ environment:
9
+ POSTGRES_USER: marketplace
10
+ POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme123}
11
+ POSTGRES_DB: marketplace
12
+ volumes:
13
+ - postgres_data:/var/lib/postgresql/data
14
+ ports:
15
+ - "5432:5432"
16
+ healthcheck:
17
+ test: ["CMD-SHELL", "pg_isready -U marketplace"]
18
+ interval: 10s
19
+ timeout: 5s
20
+ retries: 5
21
+ restart: unless-stopped
22
+
23
+ # Redis cache (optional, for future features)
24
+ redis:
25
+ image: redis:7-alpine
26
+ container_name: marketplace-redis
27
+ ports:
28
+ - "6379:6379"
29
+ volumes:
30
+ - redis_data:/data
31
+ healthcheck:
32
+ test: ["CMD", "redis-cli", "ping"]
33
+ interval: 10s
34
+ timeout: 5s
35
+ retries: 5
36
+ restart: unless-stopped
37
+
38
+ # FastAPI application
39
+ app:
40
+ build:
41
+ context: .
42
+ dockerfile: Dockerfile
43
+ container_name: marketplace-app
44
+ environment:
45
+ # Server
46
+ PORT: 3000
47
+
48
+ # Database
49
+ DATABASE_URL: postgresql://marketplace:${POSTGRES_PASSWORD:-changeme123}@postgres:5432/marketplace
50
+
51
+ # Model
52
+ MODEL_REPO_ID: unsloth/Qwen3-4B-Instruct-2507
53
+ HF_TOKEN: ${HF_TOKEN:-}
54
+ EAGER_LOAD_MODEL: ${EAGER_LOAD_MODEL:-1}
55
+
56
+ # Inference
57
+ MAX_TOKENS: 4096
58
+ TEMPERATURE: 0.7
59
+ DEVICE_MAP: auto
60
+ TORCH_DTYPE: auto
61
+
62
+ # Authentication
63
+ JWT_SECRET_KEY: ${JWT_SECRET_KEY:-your-super-secret-jwt-key-change-in-production}
64
+ ACCESS_TOKEN_EXPIRE_MINUTES: 30
65
+ REFRESH_TOKEN_EXPIRE_DAYS: 7
66
+
67
+ # Session persistence
68
+ PERSIST_SESSIONS: 1
69
+ SESSIONS_DB_PATH: sessions.db
70
+ SESSIONS_TTL_SECONDS: 600
71
+
72
+ # Redis (future)
73
+ REDIS_URL: redis://redis:6379/0
74
+ ports:
75
+ - "3000:3000"
76
+ volumes:
77
+ - ./marketplace.db:/app/marketplace.db
78
+ - ./sessions.db:/app/sessions.db
79
+ - hf_cache:/app/hf-cache
80
+ depends_on:
81
+ postgres:
82
+ condition: service_healthy
83
+ redis:
84
+ condition: service_healthy
85
+ healthcheck:
86
+ test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
87
+ interval: 30s
88
+ timeout: 10s
89
+ retries: 3
90
+ start_period: 120s
91
+ restart: unless-stopped
92
+
93
+ # Nginx reverse proxy (optional, for production)
94
+ nginx:
95
+ image: nginx:alpine
96
+ container_name: marketplace-nginx
97
+ ports:
98
+ - "80:80"
99
+ - "443:443"
100
+ volumes:
101
+ - ./nginx.conf:/etc/nginx/nginx.conf:ro
102
+ - ./ssl:/etc/nginx/ssl:ro
103
+ depends_on:
104
+ - app
105
+ restart: unless-stopped
106
+
107
+ volumes:
108
+ postgres_data:
109
+ driver: local
110
+ redis_data:
111
+ driver: local
112
+ hf_cache:
113
+ driver: local
main.py CHANGED
@@ -1900,6 +1900,143 @@ def cancel_session(session_id: str):
1900
  return JSONResponse({"ok": True, "session_id": session_id})
1901
 
1902
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1903
  # ============================================================================
1904
  # MARKETPLACE API ENDPOINTS
1905
  # ============================================================================
 
1900
  return JSONResponse({"ok": True, "session_id": session_id})
1901
 
1902
 
1903
+ # ============================================================================
1904
+ # AUTHENTICATION API ENDPOINTS
1905
+ # ============================================================================
1906
+
1907
+ from auth import (
1908
+ hash_password,
1909
+ verify_password,
1910
+ create_tokens_for_user,
1911
+ get_current_user,
1912
+ get_current_active_user,
1913
+ require_admin,
1914
+ require_supplier,
1915
+ Token,
1916
+ UserLogin,
1917
+ refresh_access_token
1918
+ )
1919
+
1920
+ class AuthUserRegister(BaseModel):
1921
+ name: str
1922
+ email: str
1923
+ password: str
1924
+ city: Optional[str] = None
1925
+ latitude: Optional[float] = None
1926
+ longitude: Optional[float] = None
1927
+ role: str = "user" # user, supplier, admin
1928
+
1929
+
1930
+ @app.post("/api/auth/register", tags=["auth"], response_model=Token)
1931
+ def register_auth_user(user: AuthUserRegister, db: Session = Depends(get_db)):
1932
+ """
1933
+ Register new user with authentication.
1934
+
1935
+ Creates user account with hashed password and returns JWT tokens.
1936
+ Role can be: user, supplier, or admin.
1937
+ """
1938
+ # Check if email exists
1939
+ existing = db.query(User).filter(User.email == user.email).first()
1940
+ if existing:
1941
+ raise HTTPException(status_code=400, detail="Email already registered")
1942
+
1943
+ # Hash password
1944
+ hashed_password = hash_password(user.password)
1945
+
1946
+ # Create user
1947
+ db_user = User(
1948
+ name=user.name,
1949
+ email=user.email,
1950
+ hashed_password=hashed_password,
1951
+ city=user.city,
1952
+ latitude=user.latitude,
1953
+ longitude=user.longitude,
1954
+ role=user.role,
1955
+ ai_access_enabled=(user.role in ["supplier", "admin"]) # Premium by default
1956
+ )
1957
+ db.add(db_user)
1958
+ db.commit()
1959
+ db.refresh(db_user)
1960
+
1961
+ # Create tokens
1962
+ tokens = create_tokens_for_user(
1963
+ email=db_user.email,
1964
+ role=db_user.role,
1965
+ user_id=db_user.id
1966
+ )
1967
+
1968
+ return tokens
1969
+
1970
+
1971
+ @app.post("/api/auth/login", tags=["auth"], response_model=Token)
1972
+ def login(credentials: UserLogin, db: Session = Depends(get_db)):
1973
+ """
1974
+ Login user and return JWT tokens.
1975
+
1976
+ Authenticates user with email and password, returns access and refresh tokens.
1977
+ """
1978
+ # Find user
1979
+ user = db.query(User).filter(User.email == credentials.email).first()
1980
+
1981
+ if not user or not user.hashed_password:
1982
+ raise HTTPException(
1983
+ status_code=status.HTTP_401_UNAUTHORIZED,
1984
+ detail="Incorrect email or password"
1985
+ )
1986
+
1987
+ # Verify password
1988
+ if not verify_password(credentials.password, user.hashed_password):
1989
+ raise HTTPException(
1990
+ status_code=status.HTTP_401_UNAUTHORIZED,
1991
+ detail="Incorrect email or password"
1992
+ )
1993
+
1994
+ # Create tokens
1995
+ tokens = create_tokens_for_user(
1996
+ email=user.email,
1997
+ role=user.role,
1998
+ user_id=user.id
1999
+ )
2000
+
2001
+ return tokens
2002
+
2003
+
2004
+ @app.post("/api/auth/refresh", tags=["auth"])
2005
+ def refresh_token(refresh_token: str):
2006
+ """
2007
+ Refresh access token using refresh token.
2008
+
2009
+ Returns new access token without requiring re-authentication.
2010
+ """
2011
+ try:
2012
+ new_access_token = refresh_access_token(refresh_token)
2013
+ return {"access_token": new_access_token, "token_type": "bearer"}
2014
+ except HTTPException as e:
2015
+ raise e
2016
+
2017
+
2018
+ @app.get("/api/auth/me", tags=["auth"])
2019
+ def get_me(current_user: dict = Depends(get_current_active_user), db: Session = Depends(get_db)):
2020
+ """
2021
+ Get current authenticated user profile.
2022
+
2023
+ Requires valid JWT token in Authorization header.
2024
+ """
2025
+ user = db.query(User).filter(User.email == current_user["email"]).first()
2026
+ if not user:
2027
+ raise HTTPException(status_code=404, detail="User not found")
2028
+
2029
+ return {
2030
+ "id": user.id,
2031
+ "name": user.name,
2032
+ "email": user.email,
2033
+ "city": user.city,
2034
+ "role": user.role,
2035
+ "ai_access_enabled": user.ai_access_enabled,
2036
+ "created_at": user.created_at
2037
+ }
2038
+
2039
+
2040
  # ============================================================================
2041
  # MARKETPLACE API ENDPOINTS
2042
  # ============================================================================
models.py CHANGED
@@ -26,6 +26,7 @@ class Supplier(Base):
26
  name = Column(String(255), nullable=False)
27
  business_name = Column(String(255), nullable=False)
28
  email = Column(String(255), unique=True, nullable=False, index=True)
 
29
  phone = Column(String(50))
30
  address = Column(Text)
31
  latitude = Column(Float, nullable=False) # For location-based search
@@ -75,11 +76,13 @@ class User(Base):
75
  id = Column(Integer, primary_key=True, index=True)
76
  name = Column(String(255), nullable=False)
77
  email = Column(String(255), unique=True, nullable=False, index=True)
 
78
  phone = Column(String(50))
79
  latitude = Column(Float)
80
  longitude = Column(Float)
81
  city = Column(String(100))
82
  province = Column(String(100))
 
83
  ai_access_enabled = Column(Boolean, default=False) # Premium feature flag
84
  preferences = Column(Text) # JSON string for user preferences
85
  created_at = Column(DateTime, default=datetime.utcnow)
 
26
  name = Column(String(255), nullable=False)
27
  business_name = Column(String(255), nullable=False)
28
  email = Column(String(255), unique=True, nullable=False, index=True)
29
+ hashed_password = Column(String(255)) # For authentication
30
  phone = Column(String(50))
31
  address = Column(Text)
32
  latitude = Column(Float, nullable=False) # For location-based search
 
76
  id = Column(Integer, primary_key=True, index=True)
77
  name = Column(String(255), nullable=False)
78
  email = Column(String(255), unique=True, nullable=False, index=True)
79
+ hashed_password = Column(String(255)) # For authentication
80
  phone = Column(String(50))
81
  latitude = Column(Float)
82
  longitude = Column(Float)
83
  city = Column(String(100))
84
  province = Column(String(100))
85
+ role = Column(String(20), default="user") # user, supplier, admin
86
  ai_access_enabled = Column(Boolean, default=False) # Premium feature flag
87
  preferences = Column(Text) # JSON string for user preferences
88
  created_at = Column(DateTime, default=datetime.utcnow)
requirements.txt CHANGED
@@ -7,6 +7,10 @@ python-multipart>=0.0.6
7
  sqlalchemy>=2.0.0
8
  alembic>=1.12.0
9
 
 
 
 
 
10
  # HF ecosystem
11
  transformers>=4.44.0
12
  accelerate>=0.33.0
 
7
  sqlalchemy>=2.0.0
8
  alembic>=1.12.0
9
 
10
+ # Authentication
11
+ python-jose[cryptography]>=3.3.0
12
+ passlib[bcrypt]>=1.7.4
13
+
14
  # HF ecosystem
15
  transformers>=4.44.0
16
  accelerate>=0.33.0
tests/test_api.py CHANGED
@@ -660,28 +660,32 @@ def test_stream_resume_data_integrity_with_unicode():
660
  assert actual == expected, f"Content mismatch: '{actual}' != '{expected}'"
661
 
662
  def test_ktp_ocr_success():
663
- with patched_engine() as fake_engine:
664
- # Configure the fake engine to return a specific JSON structure for this test
665
- expected_json = {
666
- "nik": "1234567890123456",
667
- "nama": "JOHN DOE",
668
- "tempat_lahir": "JAKARTA",
669
- "tgl_lahir": "01-01-1990",
670
- "jenis_kelamin": "LAKI-LAKI",
671
- "alamat": {
672
- "name": "JL. JEND. SUDIRMAN KAV. 52-53",
673
- "rt_rw": "001/001",
674
- "kel_desa": "SENAYAN",
675
- "kecamatan": "KEBAYORAN BARU",
676
- },
677
- "agama": "ISLAM",
678
- "status_perkawinan": "KAWIN",
679
- "pekerjaan": "PEGAWAI SWASTA",
680
- "kewarganegaraan": "WNI",
681
- "berlaku_hingga": "SEUMUR HIDUP",
682
- }
683
- fake_engine.infer = lambda messages, max_tokens, temperature: json.dumps(expected_json)
 
 
 
684
 
 
685
  client = get_client()
686
  with open("image.jpg", "rb") as f:
687
  files = {"image": ("image.jpg", f, "image/jpeg")}
@@ -690,4 +694,19 @@ def test_ktp_ocr_success():
690
  assert r.status_code == 200
691
  body = r.json()
692
  assert body["nik"] == "1234567890123456"
693
- assert body["nama"] == "JOHN DOE"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
660
  assert actual == expected, f"Content mismatch: '{actual}' != '{expected}'"
661
 
662
  def test_ktp_ocr_success():
663
+ # Mock RapidOCR to return test text lines that should parse to expected KTP data
664
+ test_ocr_texts = [
665
+ "NIK : 1234567890123456",
666
+ "Nama : JOHN DOE",
667
+ "Tempat/Tgl Lahir : JAKARTA, 01-01-1990",
668
+ "Jenis Kelamin : LAKI-LAKI",
669
+ "Alamat : JL. JEND. SUDIRMAN KAV. 52-53",
670
+ "RT/RW : 001/001",
671
+ "Kel/Desa : SENAYAN",
672
+ "Kecamatan : KEBAYORAN BARU",
673
+ "Agama : ISLAM",
674
+ "Status Perkawinan : KAWIN",
675
+ "Pekerjaan : PEGAWAI SWASTA",
676
+ "Kewarganegaraan : WNI",
677
+ "Berlaku Hingga : SEUMUR HIDUP"
678
+ ]
679
+
680
+ # Mock the OCR result format: [[(bbox, text, confidence), ...]]
681
+ mock_ocr_result = [[(None, text, 0.9) for text in test_ocr_texts]]
682
+
683
+ # Patch get_ocr_engine to return a mock OCR engine
684
+ original_get_ocr_engine = main.get_ocr_engine
685
+ mock_engine = lambda img: mock_ocr_result
686
+ main.get_ocr_engine = lambda: mock_engine
687
 
688
+ try:
689
  client = get_client()
690
  with open("image.jpg", "rb") as f:
691
  files = {"image": ("image.jpg", f, "image/jpeg")}
 
694
  assert r.status_code == 200
695
  body = r.json()
696
  assert body["nik"] == "1234567890123456"
697
+ assert body["nama"] == "John Doe"
698
+ assert body["tempat_lahir"] == "Jakarta"
699
+ assert body["tgl_lahir"] == "01-01-1990"
700
+ assert body["jenis_kelamin"] == "LAKI-LAKI"
701
+ assert body["alamat"]["name"] == "JL. JEND. SUDIRMAN KAV. 52-53"
702
+ assert body["alamat"]["rt_rw"] == "001/001"
703
+ assert body["alamat"]["kel_desa"] == "Senayan"
704
+ assert body["alamat"]["kecamatan"] == "Kebayoran Baru"
705
+ assert body["agama"] == "Islam"
706
+ assert body["status_perkawinan"] == "Kawin"
707
+ assert body["pekerjaan"] == "Pegawai Swasta"
708
+ assert body["kewarganegaraan"] == "Wni"
709
+ assert body["berlaku_hingga"] == "Seumur Hidup"
710
+ finally:
711
+ # Restore original function
712
+ main.get_ocr_engine = original_get_ocr_engine