Spaces:
Running
on
CPU Upgrade
Running
on
CPU Upgrade
File size: 14,372 Bytes
844386f 9781b82 26b1877 4335561 9781b82 4335561 26b1877 8d6faeb 5a8554e 5754eb2 56859f5 5a8554e 5754eb2 445a506 8d6faeb 944b400 8d6faeb 445a506 56859f5 8d6faeb 944b400 5754eb2 8d6faeb 445a506 9781b82 4335561 56859f5 844386f 4335561 3fb3087 4bf5083 844386f 9781b82 844386f 9781b82 844386f 944b400 a403177 5754eb2 4bf5083 56859f5 4bf5083 56859f5 5754eb2 56859f5 5754eb2 4bf5083 5754eb2 a403177 26b1877 844386f 4335561 8a99693 844386f 8a99693 844386f 90b38cc 445a506 56859f5 8a99693 844386f 8a99693 844386f 8a99693 9781b82 26b1877 844386f 4335561 844386f 4335561 8d6faeb 944b400 8d6faeb 944b400 8d6faeb 944b400 90b38cc 844386f 4335561 8a99693 844386f 4335561 844386f 4335561 844386f 4335561 844386f 9781b82 26b1877 944b400 26b1877 56859f5 8d6faeb 56859f5 944b400 56859f5 944b400 56859f5 4bf5083 56859f5 944b400 56859f5 8a99693 8d6faeb 26b1877 445a506 944b400 445a506 56859f5 4bf5083 56859f5 944b400 56859f5 944b400 4bf5083 56859f5 4bf5083 56859f5 944b400 4bf5083 56859f5 4bf5083 26b1877 8a99693 944b400 8a99693 944b400 8a99693 944b400 8a99693 |
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 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 |
import jwt
from datetime import datetime, timedelta
from fastapi import HTTPException, status, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field
from pydantic_settings import BaseSettings
from config.logging_config import logger
from sqlalchemy import create_engine, Column, String, Boolean
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from passlib.context import CryptContext
import os
import base64
from Crypto.Cipher import AES
from Crypto.Random import get_random_bytes
# SQLite database setup with Hugging Face persistent storage
DATABASE_PATH = "/data/users.db"
DATABASE_URL = f"sqlite:///{DATABASE_PATH}"
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
Base = declarative_base()
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# Model for admin-related users
class User(Base):
__tablename__ = "users"
username = Column(String, primary_key=True, index=True)
password = Column(String) # Stores hashed passwords
is_admin = Column(Boolean, default=False)
session_key = Column(String, nullable=True) # Stores base64-encoded session key
# Model for app users
class AppUser(Base):
__tablename__ = "app_users"
username = Column(String, primary_key=True, index=True)
password = Column(String) # Stores hashed passwords
session_key = Column(String, nullable=True) # Stores base64-encoded session key
# Ensure the /data directory exists
os.makedirs(os.path.dirname(DATABASE_PATH), exist_ok=True)
# Create database tables
Base.metadata.create_all(bind=engine)
# Password hashing
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class Settings(BaseSettings):
api_key_secret: str = Field(..., env="API_KEY_SECRET")
token_expiration_minutes: int = Field(1440, env="TOKEN_EXPIRATION_MINUTES")
refresh_token_expiration_days: int = Field(7, env="REFRESH_TOKEN_EXPIRATION_DAYS")
llm_model_name: str = "google/gemma-3-4b-it"
max_tokens: int = 512
host: str = "0.0.0.0"
port: int = 7860
chat_rate_limit: str = "100/minute"
speech_rate_limit: str = "5/minute"
external_tts_url: str = Field(..., env="EXTERNAL_TTS_URL")
external_asr_url: str = Field(..., env="EXTERNAL_ASR_URL")
external_text_gen_url: str = Field(..., env="EXTERNAL_TEXT_GEN_URL")
external_audio_proc_url: str = Field(..., env="EXTERNAL_AUDIO_PROC_URL")
default_admin_username: str = Field("admin", env="DEFAULT_ADMIN_USERNAME")
default_admin_password: str = Field("admin54321", env="DEFAULT_ADMIN_PASSWORD")
database_path: str = DATABASE_PATH
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
settings = Settings()
# Seed initial data for users table only
def seed_initial_data():
db = SessionLocal()
try:
test_username = "[email protected]"
if not db.query(User).filter_by(username=test_username).first():
test_device_token = "550e8400-e29b-41d4-a716-446655440000"
hashed_password = pwd_context.hash(test_device_token)
session_key = base64.b64encode(get_random_bytes(16)).decode('utf-8')
db.add(User(username=test_username, password=hashed_password, is_admin=False, session_key=session_key))
db.commit()
admin_username = settings.default_admin_username
admin_password = settings.default_admin_password
if not db.query(User).filter_by(username=admin_username).first():
hashed_password = pwd_context.hash(admin_password)
session_key = base64.b64encode(get_random_bytes(16)).decode('utf-8')
db.add(User(username=admin_username, password=hashed_password, is_admin=True, session_key=session_key))
db.commit()
logger.info(f"Seeded initial data: test user '{test_username}', admin user '{admin_username}'")
except Exception as e:
logger.error(f"Error seeding initial data: {str(e)}")
db.rollback()
finally:
db.close()
seed_initial_data()
bearer_scheme = HTTPBearer()
class TokenPayload(BaseModel):
sub: str
exp: float
type: str
class TokenResponse(BaseModel):
access_token: str
refresh_token: str
token_type: str
class LoginRequest(BaseModel):
username: str
password: str
class RegisterRequest(BaseModel):
username: str
password: str
def decrypt_data(encrypted_data: str, key: bytes) -> str:
try:
data = base64.b64decode(encrypted_data)
nonce, ciphertext = data[:12], data[12:]
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
plaintext = cipher.decrypt_and_verify(ciphertext[:-16], ciphertext[-16:])
return plaintext.decode('utf-8')
except Exception as e:
logger.error(f"Decryption failed: {str(e)}")
raise HTTPException(status_code=400, detail="Invalid encrypted data")
async def create_access_token(user_id: str) -> dict:
expire = datetime.utcnow() + timedelta(minutes=settings.token_expiration_minutes)
payload = {"sub": user_id, "exp": expire.timestamp(), "type": "access"}
token = jwt.encode(payload, settings.api_key_secret, algorithm="HS256")
refresh_expire = datetime.utcnow() + timedelta(days=settings.refresh_token_expiration_days)
refresh_payload = {"sub": user_id, "exp": refresh_expire.timestamp(), "type": "refresh"}
refresh_token = jwt.encode(refresh_payload, settings.api_key_secret, algorithm="HS256")
logger.info(f"Generated tokens for user: {user_id}")
return {"access_token": token, "refresh_token": refresh_token}
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> str:
token = credentials.credentials
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.api_key_secret, algorithms=["HS256"], options={"verify_exp": False})
token_data = TokenPayload(**payload)
user_id = token_data.sub
db = SessionLocal()
# Check both users and app_users tables
user = db.query(User).filter_by(username=user_id).first()
app_user = db.query(AppUser).filter_by(username=user_id).first()
db.close()
if user_id is None or (not user and not app_user):
logger.warning(f"Invalid or unknown user: {user_id}")
raise credentials_exception
current_time = datetime.utcnow().timestamp()
if current_time > token_data.exp:
logger.warning(f"Token expired: current_time={current_time}, exp={token_data.exp}")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token has expired",
headers={"WWW-Authenticate": "Bearer"},
)
logger.info(f"Validated token for user: {user_id}")
return user_id
except jwt.InvalidSignatureError as e:
logger.error(f"Invalid signature error: {str(e)}")
raise credentials_exception
except jwt.InvalidTokenError as e:
logger.error(f"Other token error: {str(e)}")
raise credentials_exception
except Exception as e:
logger.error(f"Unexpected token validation error: {str(e)}")
raise credentials_exception
async def get_current_user_with_admin(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> str:
token = credentials.credentials
credentials_exception = HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid authentication credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(token, settings.api_key_secret, algorithms=["HS256"])
token_data = TokenPayload(**payload)
user_id = token_data.sub
db = SessionLocal()
user = db.query(User).filter_by(username=user_id).first()
db.close()
if not user:
logger.warning(f"User not found in users table: {user_id}")
raise credentials_exception
if not user.is_admin:
logger.warning(f"User {user_id} is not authorized as admin")
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin access required; only admin accounts can perform this action"
)
logger.info(f"Validated admin user: {user_id}")
return user_id
except jwt.InvalidSignatureError as e:
logger.error(f"Invalid signature error: {str(e)}")
raise credentials_exception
except jwt.InvalidTokenError as e:
logger.error(f"Other token error: {str(e)}")
raise credentials_exception
except Exception as e:
logger.error(f"Unexpected admin validation error: {str(e)}")
raise credentials_exception
async def login(login_request: LoginRequest, session_key_b64: str) -> TokenResponse:
db = SessionLocal()
session_key = base64.b64decode(session_key_b64)
try:
username = decrypt_data(login_request.username, session_key)
password = decrypt_data(login_request.password, session_key)
except:
db.close()
raise HTTPException(status_code=400, detail="Invalid encrypted data")
# Check both users and app_users tables
user = db.query(User).filter_by(username=username).first()
app_user = db.query(AppUser).filter_by(username=username).first()
if not user and not app_user:
db.close()
logger.warning(f"Login failed for user: {username}")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or device token")
target_user = user if user else app_user
if not pwd_context.verify(password, target_user.password):
db.close()
logger.warning(f"Login failed for user: {username}")
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or device token")
if target_user.session_key != session_key_b64:
target_user.session_key = session_key_b64
db.commit()
db.close()
tokens = await create_access_token(user_id=username)
return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
async def register(register_request: RegisterRequest, current_user: str = Depends(get_current_user_with_admin)) -> TokenResponse:
db = SessionLocal()
try:
existing_user = db.query(User).filter_by(username=register_request.username).first()
if existing_user:
logger.warning(f"Registration failed: Username {register_request.username} already exists")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already exists")
hashed_password = pwd_context.hash(register_request.password)
new_user = User(username=register_request.username, password=hashed_password, is_admin=False)
db.add(new_user)
db.commit()
logger.info(f"Admin {current_user} successfully registered new user: {register_request.username}")
tokens = await create_access_token(user_id=register_request.username)
return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
except Exception as e:
db.rollback()
logger.error(f"Registration error by admin {current_user}: {str(e)}")
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Registration failed: {str(e)}")
finally:
db.close()
async def app_register(register_request: RegisterRequest, session_key_b64: str) -> TokenResponse:
db = SessionLocal()
session_key = base64.b64decode(session_key_b64)
try:
username = decrypt_data(register_request.username, session_key)
password = decrypt_data(register_request.password, session_key)
except:
db.close()
raise HTTPException(status_code=400, detail="Invalid encrypted data")
# Check both tables to prevent duplicate usernames
existing_user = db.query(User).filter_by(username=username).first()
existing_app_user = db.query(AppUser).filter_by(username=username).first()
if existing_user or existing_app_user:
db.close()
logger.warning(f"App registration failed: Email {username} already exists")
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
hashed_password = pwd_context.hash(password)
new_app_user = AppUser(username=username, password=hashed_password, session_key=session_key_b64)
db.add(new_app_user)
db.commit()
db.close()
tokens = await create_access_token(user_id=username)
logger.info(f"App registered new user: {username}")
return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
async def refresh_token(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> TokenResponse:
token = credentials.credentials
try:
payload = jwt.decode(token, settings.api_key_secret, algorithms=["HS256"])
token_data = TokenPayload(**payload)
if payload.get("type") != "refresh":
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type; refresh token required")
user_id = token_data.sub
db = SessionLocal()
# Check both users and app_users tables
user = db.query(User).filter_by(username=user_id).first()
app_user = db.query(AppUser).filter_by(username=user_id).first()
db.close()
if not user and not app_user:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
tokens = await create_access_token(user_id=user_id)
return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
except jwt.InvalidTokenError:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token") |