sachin
commited on
Commit
·
a9fdc8f
1
Parent(s):
89878fd
update-proxy
Browse files- .env.server +0 -9
- docs/menv.md +0 -9
- docs/tranlate.md +0 -66
- requirements.txt +0 -8
- src/server/config/logging_config.py +0 -35
- src/server/config/tts_config.py +0 -27
- src/server/main.py +3 -2
- src/server/utils/auth.py +0 -323
- src/server/utils/crypto.py +0 -13
- src/server/utils/text.py +0 -3
.env.server
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
PORT=7860
|
2 |
-
HOST=0.0.0.0
|
3 |
-
SPEECH_RATE_LIMIT=5/minute
|
4 |
-
CHAT_RATE_LIMIT=100/minute
|
5 |
-
EXTERNAL_TTS_URL=https://slabstech-dhwani-internal-api-server.hf.space/v1/audio/speech
|
6 |
-
EXTERNAL_ASR_URL=https://gaganyatri-asr-indic-server-cpu.hf.space
|
7 |
-
EXTERNAL_TEXT_GEN_URL=https://slabstech-dhwani-internal-api-server.hf.space/v1/audio/speech
|
8 |
-
EXTERNAL_AUDIO_PROC_URL=https://slabstech-dhwani-internal-api-server.hf.space/v1/audio/speech
|
9 |
-
API_KEY_SECRET=your_secret_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/menv.md
DELETED
@@ -1,9 +0,0 @@
|
|
1 |
-
export PORT=7860
|
2 |
-
export HOST=0.0.0.0
|
3 |
-
export SPEECH_RATE_LIMIT=5/minute
|
4 |
-
export CHAT_RATE_LIMIT=100/minute
|
5 |
-
export EXTERNAL_TTS_URL=https://slabstech-dhwani-internal-api-server.hf.space/v1/audio/speech
|
6 |
-
export EXTERNAL_ASR_URL=https://gaganyatri-asr-indic-server-cpu.hf.space
|
7 |
-
export EXTERNAL_TEXT_GEN_URL=https://slabstech-dhwani-internal-api-server.hf.space
|
8 |
-
export EXTERNAL_AUDIO_PROC_URL=https://slabstech-dhwani-internal-api-server.hf.space
|
9 |
-
export API_KEY_SECRET=your_secret_key
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
docs/tranlate.md
DELETED
@@ -1,66 +0,0 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
curl -X 'POST' \
|
4 |
-
'https://gaganyatri-translate-indic-server-cpu.hf.space/translate?src_lang=eng_Latn&tgt_lang=kan_Knda' \
|
5 |
-
-H 'accept: application/json' \
|
6 |
-
-H 'Content-Type: application/json' \
|
7 |
-
-d '{
|
8 |
-
"sentences": [
|
9 |
-
"Hello, how are you?", "Good morning!"
|
10 |
-
],
|
11 |
-
"src_lang": "eng_Latn",
|
12 |
-
"tgt_lang": "kan_Knda"
|
13 |
-
}'
|
14 |
-
|
15 |
-
|
16 |
-
{
|
17 |
-
"translations": [
|
18 |
-
"ಹಲೋ, ಹೇಗಿದ್ದೀರಿ? ",
|
19 |
-
"ಶುಭೋದಯ! "
|
20 |
-
]
|
21 |
-
}
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
curl -X 'POST' \
|
27 |
-
'https://gaganyatri-translate-indic-server-cpu.hf.space/translate?src_lang=kan_Knda&tgt_lang=eng_Latn' \
|
28 |
-
-H 'accept: application/json' \
|
29 |
-
-H 'Content-Type: application/json' \
|
30 |
-
-d '{
|
31 |
-
"sentences": [
|
32 |
-
"ನಮಸ್ಕಾರ, ಹೇಗಿದ್ದೀರಾ?", "ಶುಭೋದಯ!"
|
33 |
-
],
|
34 |
-
"src_lang": "kan_Knda",
|
35 |
-
"tgt_lang": "eng_Latn"
|
36 |
-
}'
|
37 |
-
|
38 |
-
|
39 |
-
{
|
40 |
-
"translations": [
|
41 |
-
"Hello, how are you?",
|
42 |
-
"Good morning!"
|
43 |
-
]
|
44 |
-
}
|
45 |
-
|
46 |
-
|
47 |
-
|
48 |
-
curl -X 'POST' \
|
49 |
-
'https://gaganyatri-translate-indic-server-cpu.hf.space/translate?src_lang=kan_Knda&tgt_lang=hin_Deva' \
|
50 |
-
-H 'accept: application/json' \
|
51 |
-
-H 'Content-Type: application/json' \
|
52 |
-
-d '{
|
53 |
-
"sentences": [
|
54 |
-
"ನಮಸ್ಕಾರ, ಹೇಗಿದ್ದೀರಾ?", "ಶುಭೋದಯ!"
|
55 |
-
],
|
56 |
-
"src_lang": "kan_Knda",
|
57 |
-
"tgt_lang": "hin_Deva"
|
58 |
-
}'
|
59 |
-
|
60 |
-
|
61 |
-
{
|
62 |
-
"translations": [
|
63 |
-
"हैलो, कैसा लग रहा है? ",
|
64 |
-
"गुड मॉर्निंग! "
|
65 |
-
]
|
66 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
requirements.txt
CHANGED
@@ -1,12 +1,4 @@
|
|
1 |
uvicorn
|
2 |
fastapi
|
3 |
-
pydantic_settings
|
4 |
-
slowapi
|
5 |
requests
|
6 |
-
python-multipart
|
7 |
-
pillow
|
8 |
-
pyjwt
|
9 |
-
sqlalchemy
|
10 |
-
passlib[bcrypt]
|
11 |
-
pycryptodome
|
12 |
httpx
|
|
|
1 |
uvicorn
|
2 |
fastapi
|
|
|
|
|
3 |
requests
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
httpx
|
src/server/config/logging_config.py
DELETED
@@ -1,35 +0,0 @@
|
|
1 |
-
import logging
|
2 |
-
import logging.config
|
3 |
-
from logging.handlers import RotatingFileHandler
|
4 |
-
from .tts_config import config
|
5 |
-
|
6 |
-
logging_config = {
|
7 |
-
"version": 1,
|
8 |
-
"disable_existing_loggers": False,
|
9 |
-
"formatters": {
|
10 |
-
"simple": {"format": "%(asctime)s - %(name)s - %(levelname)s - %(message)s"},
|
11 |
-
},
|
12 |
-
"handlers": {
|
13 |
-
"stdout": {
|
14 |
-
"class": "logging.StreamHandler",
|
15 |
-
"formatter": "simple",
|
16 |
-
"stream": "ext://sys.stdout",
|
17 |
-
},
|
18 |
-
"file": {
|
19 |
-
"class": "logging.handlers.RotatingFileHandler",
|
20 |
-
"formatter": "simple",
|
21 |
-
"filename": "dhwani_api.log",
|
22 |
-
"maxBytes": 10 * 1024 * 1024, # 10MB
|
23 |
-
"backupCount": 5,
|
24 |
-
},
|
25 |
-
},
|
26 |
-
"loggers": {
|
27 |
-
"root": {
|
28 |
-
"level": config.log_level.upper(),
|
29 |
-
"handlers": ["stdout", "file"],
|
30 |
-
},
|
31 |
-
},
|
32 |
-
}
|
33 |
-
|
34 |
-
logging.config.dictConfig(logging_config)
|
35 |
-
logger = logging.getLogger("indic_all_server")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/server/config/tts_config.py
DELETED
@@ -1,27 +0,0 @@
|
|
1 |
-
import enum
|
2 |
-
from pydantic_settings import BaseSettings
|
3 |
-
|
4 |
-
SPEED = 1.0
|
5 |
-
|
6 |
-
class StrEnum(str, enum.Enum):
|
7 |
-
def __str__(self):
|
8 |
-
return str(self.value)
|
9 |
-
|
10 |
-
class ResponseFormat(StrEnum):
|
11 |
-
MP3 = "mp3"
|
12 |
-
FLAC = "flac"
|
13 |
-
WAV = "wav"
|
14 |
-
|
15 |
-
class Config(BaseSettings):
|
16 |
-
log_level: str = "info"
|
17 |
-
model: str = "ai4bharat/indic-parler-tts"
|
18 |
-
max_models: int = 1
|
19 |
-
lazy_load_model: bool = False # Unused now, as all models are lazy-loaded
|
20 |
-
input: str = "ನಿಮ್ಮ ಇನ್ಪುಟ್ ಪಠ್ಯವನ್ನು ಇಲ್ಲಿ ಸೇರಿಸಿ"
|
21 |
-
voice: str = (
|
22 |
-
"Female speaks with a high pitch at a normal pace in a clear, close-sounding environment. "
|
23 |
-
"Her neutral tone is captured with excellent audio quality."
|
24 |
-
)
|
25 |
-
response_format: ResponseFormat = ResponseFormat.MP3
|
26 |
-
|
27 |
-
config = Config()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/server/main.py
CHANGED
@@ -1,7 +1,7 @@
|
|
1 |
from fastapi import FastAPI, Request, HTTPException
|
2 |
from fastapi.responses import Response
|
3 |
import httpx
|
4 |
-
|
5 |
# FastAPI app setup
|
6 |
app = FastAPI(
|
7 |
title="Dhwani API Proxy",
|
@@ -11,7 +11,8 @@ app = FastAPI(
|
|
11 |
)
|
12 |
|
13 |
# Target server to forward requests to
|
14 |
-
TARGET_SERVER =
|
|
|
15 |
|
16 |
# Catch-all route to forward all requests
|
17 |
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
|
|
|
1 |
from fastapi import FastAPI, Request, HTTPException
|
2 |
from fastapi.responses import Response
|
3 |
import httpx
|
4 |
+
import os
|
5 |
# FastAPI app setup
|
6 |
app = FastAPI(
|
7 |
title="Dhwani API Proxy",
|
|
|
11 |
)
|
12 |
|
13 |
# Target server to forward requests to
|
14 |
+
TARGET_SERVER = os.getenv("DWANI_API_BASE_URL") # Replace with the actual target server IP and port
|
15 |
+
|
16 |
|
17 |
# Catch-all route to forward all requests
|
18 |
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"])
|
src/server/utils/auth.py
DELETED
@@ -1,323 +0,0 @@
|
|
1 |
-
import jwt
|
2 |
-
from datetime import datetime, timedelta
|
3 |
-
from fastapi import HTTPException, status, Depends
|
4 |
-
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
5 |
-
from pydantic import BaseModel, Field
|
6 |
-
from pydantic_settings import BaseSettings
|
7 |
-
from config.logging_config import logger
|
8 |
-
from sqlalchemy import create_engine, Column, String, Boolean
|
9 |
-
from sqlalchemy.ext.declarative import declarative_base
|
10 |
-
from sqlalchemy.orm import sessionmaker
|
11 |
-
from passlib.context import CryptContext
|
12 |
-
import os
|
13 |
-
import base64
|
14 |
-
from Crypto.Cipher import AES
|
15 |
-
from Crypto.Random import get_random_bytes
|
16 |
-
|
17 |
-
# SQLite database setup with Hugging Face persistent storage
|
18 |
-
DATABASE_PATH = "/data/users.db"
|
19 |
-
DATABASE_URL = f"sqlite:///{DATABASE_PATH}"
|
20 |
-
engine = create_engine(DATABASE_URL, connect_args={"check_same_thread": False})
|
21 |
-
Base = declarative_base()
|
22 |
-
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
23 |
-
|
24 |
-
# Model for admin-related users
|
25 |
-
class User(Base):
|
26 |
-
__tablename__ = "users"
|
27 |
-
username = Column(String, primary_key=True, index=True)
|
28 |
-
password = Column(String) # Stores hashed passwords
|
29 |
-
is_admin = Column(Boolean, default=False)
|
30 |
-
session_key = Column(String, nullable=True) # Stores base64-encoded session key
|
31 |
-
|
32 |
-
# Model for app users
|
33 |
-
class AppUser(Base):
|
34 |
-
__tablename__ = "app_users"
|
35 |
-
username = Column(String, primary_key=True, index=True)
|
36 |
-
password = Column(String) # Stores hashed passwords
|
37 |
-
session_key = Column(String, nullable=True) # Stores base64-encoded session key
|
38 |
-
|
39 |
-
# Ensure the /data directory exists
|
40 |
-
os.makedirs(os.path.dirname(DATABASE_PATH), exist_ok=True)
|
41 |
-
|
42 |
-
# Create database tables
|
43 |
-
Base.metadata.create_all(bind=engine)
|
44 |
-
|
45 |
-
# Password hashing
|
46 |
-
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
47 |
-
|
48 |
-
class Settings(BaseSettings):
|
49 |
-
api_key_secret: str = Field(..., env="API_KEY_SECRET")
|
50 |
-
token_expiration_minutes: int = Field(1440, env="TOKEN_EXPIRATION_MINUTES")
|
51 |
-
refresh_token_expiration_days: int = Field(7, env="REFRESH_TOKEN_EXPIRATION_DAYS")
|
52 |
-
llm_model_name: str = "google/gemma-3-4b-it"
|
53 |
-
max_tokens: int = 512
|
54 |
-
host: str = "0.0.0.0"
|
55 |
-
port: int = 7860
|
56 |
-
chat_rate_limit: str = "100/minute"
|
57 |
-
speech_rate_limit: str = "5/minute"
|
58 |
-
external_tts_url: str = Field(..., env="EXTERNAL_TTS_URL")
|
59 |
-
external_asr_url: str = Field(..., env="EXTERNAL_ASR_URL")
|
60 |
-
external_text_gen_url: str = Field(..., env="EXTERNAL_TEXT_GEN_URL")
|
61 |
-
external_audio_proc_url: str = Field(..., env="EXTERNAL_AUDIO_PROC_URL")
|
62 |
-
external_api_base_url: str = Field("http://localhost:7860", env="EXTERNAL_API_BASE_URL") # New field
|
63 |
-
external_pdf_api_base_url: str = Field("http://localhost:7861", env="EXTERNAL_PDF_API_BASE_URL") # New field
|
64 |
-
default_admin_username: str = Field("admin", env="DEFAULT_ADMIN_USERNAME")
|
65 |
-
default_admin_password: str = Field("admin54321", env="DEFAULT_ADMIN_PASSWORD")
|
66 |
-
database_path: str = DATABASE_PATH
|
67 |
-
|
68 |
-
class Config:
|
69 |
-
env_file = ".env"
|
70 |
-
env_file_encoding = "utf-8"
|
71 |
-
|
72 |
-
settings = Settings()
|
73 |
-
|
74 |
-
# Seed initial data for users table only
|
75 |
-
def seed_initial_data():
|
76 |
-
db = SessionLocal()
|
77 |
-
try:
|
78 |
-
test_username = "[email protected]"
|
79 |
-
if not db.query(User).filter_by(username=test_username).first():
|
80 |
-
test_device_token = "550e8400-e29b-41d4-a716-446655440000"
|
81 |
-
hashed_password = pwd_context.hash(test_device_token)
|
82 |
-
session_key = base64.b64encode(get_random_bytes(16)).decode('utf-8')
|
83 |
-
db.add(User(username=test_username, password=hashed_password, is_admin=False, session_key=session_key))
|
84 |
-
db.commit()
|
85 |
-
admin_username = settings.default_admin_username
|
86 |
-
admin_password = settings.default_admin_password
|
87 |
-
if not db.query(User).filter_by(username=admin_username).first():
|
88 |
-
hashed_password = pwd_context.hash(admin_password)
|
89 |
-
session_key = base64.b64encode(get_random_bytes(16)).decode('utf-8')
|
90 |
-
db.add(User(username=admin_username, password=hashed_password, is_admin=True, session_key=session_key))
|
91 |
-
db.commit()
|
92 |
-
logger.info(f"Seeded initial data: test user '{test_username}', admin user '{admin_username}'")
|
93 |
-
except Exception as e:
|
94 |
-
logger.error(f"Error seeding initial data: {str(e)}")
|
95 |
-
db.rollback()
|
96 |
-
finally:
|
97 |
-
db.close()
|
98 |
-
|
99 |
-
seed_initial_data()
|
100 |
-
|
101 |
-
bearer_scheme = HTTPBearer()
|
102 |
-
|
103 |
-
class TokenPayload(BaseModel):
|
104 |
-
sub: str
|
105 |
-
exp: float
|
106 |
-
type: str
|
107 |
-
|
108 |
-
class TokenResponse(BaseModel):
|
109 |
-
access_token: str
|
110 |
-
refresh_token: str
|
111 |
-
token_type: str
|
112 |
-
|
113 |
-
class LoginRequest(BaseModel):
|
114 |
-
username: str
|
115 |
-
password: str
|
116 |
-
|
117 |
-
class RegisterRequest(BaseModel):
|
118 |
-
username: str
|
119 |
-
password: str
|
120 |
-
|
121 |
-
def decrypt_data(encrypted_data: str, key: bytes) -> str:
|
122 |
-
try:
|
123 |
-
data = base64.b64decode(encrypted_data)
|
124 |
-
nonce, ciphertext = data[:12], data[12:]
|
125 |
-
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
126 |
-
plaintext = cipher.decrypt_and_verify(ciphertext[:-16], ciphertext[-16:])
|
127 |
-
return plaintext.decode('utf-8')
|
128 |
-
except Exception as e:
|
129 |
-
logger.error(f"Decryption failed: {str(e)}")
|
130 |
-
raise HTTPException(status_code=400, detail="Invalid encrypted data")
|
131 |
-
|
132 |
-
async def create_access_token(user_id: str) -> dict:
|
133 |
-
expire = datetime.utcnow() + timedelta(minutes=settings.token_expiration_minutes)
|
134 |
-
payload = {"sub": user_id, "exp": expire.timestamp(), "type": "access"}
|
135 |
-
token = jwt.encode(payload, settings.api_key_secret, algorithm="HS256")
|
136 |
-
refresh_expire = datetime.utcnow() + timedelta(days=settings.refresh_token_expiration_days)
|
137 |
-
refresh_payload = {"sub": user_id, "exp": refresh_expire.timestamp(), "type": "refresh"}
|
138 |
-
refresh_token = jwt.encode(refresh_payload, settings.api_key_secret, algorithm="HS256")
|
139 |
-
logger.info(f"Generated tokens for user: {user_id}")
|
140 |
-
return {"access_token": token, "refresh_token": refresh_token}
|
141 |
-
|
142 |
-
async def get_current_user(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> str:
|
143 |
-
token = credentials.credentials
|
144 |
-
credentials_exception = HTTPException(
|
145 |
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
146 |
-
detail="Invalid authentication credentials",
|
147 |
-
headers={"WWW-Authenticate": "Bearer"},
|
148 |
-
)
|
149 |
-
try:
|
150 |
-
payload = jwt.decode(token, settings.api_key_secret, algorithms=["HS256"], options={"verify_exp": False})
|
151 |
-
token_data = TokenPayload(**payload)
|
152 |
-
user_id = token_data.sub
|
153 |
-
|
154 |
-
db = SessionLocal()
|
155 |
-
# Check both users and app_users tables
|
156 |
-
user = db.query(User).filter_by(username=user_id).first()
|
157 |
-
app_user = db.query(AppUser).filter_by(username=user_id).first()
|
158 |
-
db.close()
|
159 |
-
if user_id is None or (not user and not app_user):
|
160 |
-
logger.warning(f"Invalid or unknown user: {user_id}")
|
161 |
-
raise credentials_exception
|
162 |
-
|
163 |
-
current_time = datetime.utcnow().timestamp()
|
164 |
-
if current_time > token_data.exp:
|
165 |
-
logger.warning(f"Token expired: current_time={current_time}, exp={token_data.exp}")
|
166 |
-
raise HTTPException(
|
167 |
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
168 |
-
detail="Token has expired",
|
169 |
-
headers={"WWW-Authenticate": "Bearer"},
|
170 |
-
)
|
171 |
-
|
172 |
-
logger.info(f"Validated token for user: {user_id}")
|
173 |
-
return user_id
|
174 |
-
except jwt.InvalidSignatureError as e:
|
175 |
-
logger.error(f"Invalid signature error: {str(e)}")
|
176 |
-
raise credentials_exception
|
177 |
-
except jwt.InvalidTokenError as e:
|
178 |
-
logger.error(f"Other token error: {str(e)}")
|
179 |
-
raise credentials_exception
|
180 |
-
except Exception as e:
|
181 |
-
logger.error(f"Unexpected token validation error: {str(e)}")
|
182 |
-
raise credentials_exception
|
183 |
-
|
184 |
-
async def get_current_user_with_admin(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> str:
|
185 |
-
token = credentials.credentials
|
186 |
-
credentials_exception = HTTPException(
|
187 |
-
status_code=status.HTTP_401_UNAUTHORIZED,
|
188 |
-
detail="Invalid authentication credentials",
|
189 |
-
headers={"WWW-Authenticate": "Bearer"},
|
190 |
-
)
|
191 |
-
try:
|
192 |
-
payload = jwt.decode(token, settings.api_key_secret, algorithms=["HS256"])
|
193 |
-
token_data = TokenPayload(**payload)
|
194 |
-
user_id = token_data.sub
|
195 |
-
|
196 |
-
db = SessionLocal()
|
197 |
-
user = db.query(User).filter_by(username=user_id).first()
|
198 |
-
db.close()
|
199 |
-
if not user:
|
200 |
-
logger.warning(f"User not found in users table: {user_id}")
|
201 |
-
raise credentials_exception
|
202 |
-
if not user.is_admin:
|
203 |
-
logger.warning(f"User {user_id} is not authorized as admin")
|
204 |
-
raise HTTPException(
|
205 |
-
status_code=status.HTTP_403_FORBIDDEN,
|
206 |
-
detail="Admin access required; only admin accounts can perform this action"
|
207 |
-
)
|
208 |
-
|
209 |
-
logger.info(f"Validated admin user: {user_id}")
|
210 |
-
return user_id
|
211 |
-
except jwt.InvalidSignatureError as e:
|
212 |
-
logger.error(f"Invalid signature error: {str(e)}")
|
213 |
-
raise credentials_exception
|
214 |
-
except jwt.InvalidTokenError as e:
|
215 |
-
logger.error(f"Other token error: {str(e)}")
|
216 |
-
raise credentials_exception
|
217 |
-
except Exception as e:
|
218 |
-
logger.error(f"Unexpected admin validation error: {str(e)}")
|
219 |
-
raise credentials_exception
|
220 |
-
|
221 |
-
async def login(login_request: LoginRequest, session_key_b64: str) -> TokenResponse:
|
222 |
-
db = SessionLocal()
|
223 |
-
session_key = base64.b64decode(session_key_b64)
|
224 |
-
try:
|
225 |
-
username = decrypt_data(login_request.username, session_key)
|
226 |
-
password = decrypt_data(login_request.password, session_key)
|
227 |
-
except:
|
228 |
-
db.close()
|
229 |
-
raise HTTPException(status_code=400, detail="Invalid encrypted data")
|
230 |
-
|
231 |
-
# Check both users and app_users tables
|
232 |
-
user = db.query(User).filter_by(username=username).first()
|
233 |
-
app_user = db.query(AppUser).filter_by(username=username).first()
|
234 |
-
|
235 |
-
if not user and not app_user:
|
236 |
-
db.close()
|
237 |
-
logger.warning(f"Login failed for user: {username}")
|
238 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or device token")
|
239 |
-
|
240 |
-
target_user = user if user else app_user
|
241 |
-
if not pwd_context.verify(password, target_user.password):
|
242 |
-
db.close()
|
243 |
-
logger.warning(f"Login failed for user: {username}")
|
244 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid email or device token")
|
245 |
-
|
246 |
-
if target_user.session_key != session_key_b64:
|
247 |
-
target_user.session_key = session_key_b64
|
248 |
-
db.commit()
|
249 |
-
db.close()
|
250 |
-
|
251 |
-
tokens = await create_access_token(user_id=username)
|
252 |
-
return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
|
253 |
-
|
254 |
-
async def register(register_request: RegisterRequest, current_user: str = Depends(get_current_user_with_admin)) -> TokenResponse:
|
255 |
-
db = SessionLocal()
|
256 |
-
try:
|
257 |
-
existing_user = db.query(User).filter_by(username=register_request.username).first()
|
258 |
-
if existing_user:
|
259 |
-
logger.warning(f"Registration failed: Username {register_request.username} already exists")
|
260 |
-
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Username already exists")
|
261 |
-
|
262 |
-
hashed_password = pwd_context.hash(register_request.password)
|
263 |
-
new_user = User(username=register_request.username, password=hashed_password, is_admin=False)
|
264 |
-
db.add(new_user)
|
265 |
-
db.commit()
|
266 |
-
logger.info(f"Admin {current_user} successfully registered new user: {register_request.username}")
|
267 |
-
|
268 |
-
tokens = await create_access_token(user_id=register_request.username)
|
269 |
-
return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
|
270 |
-
except Exception as e:
|
271 |
-
db.rollback()
|
272 |
-
logger.error(f"Registration error by admin {current_user}: {str(e)}")
|
273 |
-
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"Registration failed: {str(e)}")
|
274 |
-
finally:
|
275 |
-
db.close()
|
276 |
-
|
277 |
-
async def app_register(register_request: RegisterRequest, session_key_b64: str) -> TokenResponse:
|
278 |
-
db = SessionLocal()
|
279 |
-
session_key = base64.b64decode(session_key_b64)
|
280 |
-
try:
|
281 |
-
username = decrypt_data(register_request.username, session_key)
|
282 |
-
password = decrypt_data(register_request.password, session_key)
|
283 |
-
except:
|
284 |
-
db.close()
|
285 |
-
raise HTTPException(status_code=400, detail="Invalid encrypted data")
|
286 |
-
|
287 |
-
# Check both tables to prevent duplicate usernames
|
288 |
-
existing_user = db.query(User).filter_by(username=username).first()
|
289 |
-
existing_app_user = db.query(AppUser).filter_by(username=username).first()
|
290 |
-
if existing_user or existing_app_user:
|
291 |
-
db.close()
|
292 |
-
logger.warning(f"App registration failed: Email {username} already exists")
|
293 |
-
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered")
|
294 |
-
|
295 |
-
hashed_password = pwd_context.hash(password)
|
296 |
-
new_app_user = AppUser(username=username, password=hashed_password, session_key=session_key_b64)
|
297 |
-
db.add(new_app_user)
|
298 |
-
db.commit()
|
299 |
-
db.close()
|
300 |
-
|
301 |
-
tokens = await create_access_token(user_id=username)
|
302 |
-
logger.info(f"App registered new user: {username}")
|
303 |
-
return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
|
304 |
-
|
305 |
-
async def refresh_token(credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme)) -> TokenResponse:
|
306 |
-
token = credentials.credentials
|
307 |
-
try:
|
308 |
-
payload = jwt.decode(token, settings.api_key_secret, algorithms=["HS256"])
|
309 |
-
token_data = TokenPayload(**payload)
|
310 |
-
if payload.get("type") != "refresh":
|
311 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token type; refresh token required")
|
312 |
-
user_id = token_data.sub
|
313 |
-
db = SessionLocal()
|
314 |
-
# Check both users and app_users tables
|
315 |
-
user = db.query(User).filter_by(username=user_id).first()
|
316 |
-
app_user = db.query(AppUser).filter_by(username=user_id).first()
|
317 |
-
db.close()
|
318 |
-
if not user and not app_user:
|
319 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
320 |
-
tokens = await create_access_token(user_id=user_id)
|
321 |
-
return TokenResponse(access_token=tokens["access_token"], refresh_token=tokens["refresh_token"], token_type="bearer")
|
322 |
-
except jwt.InvalidTokenError:
|
323 |
-
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/server/utils/crypto.py
DELETED
@@ -1,13 +0,0 @@
|
|
1 |
-
from Crypto.Cipher import AES
|
2 |
-
from fastapi import HTTPException
|
3 |
-
from config.logging_config import logger
|
4 |
-
|
5 |
-
def decrypt_data(encrypted_data: bytes, key: bytes) -> bytes:
|
6 |
-
try:
|
7 |
-
nonce, ciphertext = encrypted_data[:12], encrypted_data[12:]
|
8 |
-
cipher = AES.new(key, AES.MODE_GCM, nonce=nonce)
|
9 |
-
plaintext = cipher.decrypt_and_verify(ciphertext[:-16], ciphertext[-16:])
|
10 |
-
return plaintext
|
11 |
-
except Exception as e:
|
12 |
-
logger.error(f"Decryption failed: {str(e)}")
|
13 |
-
raise HTTPException(status_code=400, detail="Invalid encrypted data")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/server/utils/text.py
DELETED
@@ -1,3 +0,0 @@
|
|
1 |
-
def chunk_text(text: str, chunk_size: int = 15) -> list[str]:
|
2 |
-
words = text.split()
|
3 |
-
return [' '.join(words[i:i + chunk_size]) for i in range(0, len(words), chunk_size)]
|
|
|
|
|
|
|
|