Server Fixes
This commit is contained in:
@@ -3,7 +3,7 @@
|
|||||||
<modelVersion>4.0.0</modelVersion>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<groupId>me.sashegdev</groupId>
|
<groupId>me.sashegdev</groupId>
|
||||||
<artifactId>ZernMCLauncher</artifactId>
|
<artifactId>ZernMCLauncher</artifactId>
|
||||||
<version>1.0.6</version>
|
<version>1.0.7</version>
|
||||||
<build>
|
<build>
|
||||||
<plugins>
|
<plugins>
|
||||||
<plugin>
|
<plugin>
|
||||||
|
|||||||
+158
-104
@@ -1,4 +1,3 @@
|
|||||||
# auth.py
|
|
||||||
import base64
|
import base64
|
||||||
import json
|
import json
|
||||||
import sqlite3
|
import sqlite3
|
||||||
@@ -38,9 +37,12 @@ def _get_secret() -> bytes:
|
|||||||
_SECRET = _get_secret()
|
_SECRET = _get_secret()
|
||||||
|
|
||||||
def create_jwt(payload: dict) -> str:
|
def create_jwt(payload: dict) -> str:
|
||||||
import base64, json
|
header = base64.urlsafe_b64encode(
|
||||||
header = base64.urlsafe_b64encode(json.dumps({"alg": "HS256", "typ": "JWT"}).encode()).rstrip(b'=').decode()
|
json.dumps({"alg": "HS256", "typ": "JWT"}).encode()
|
||||||
body = base64.urlsafe_b64encode(json.dumps(payload).encode()).rstrip(b'=').decode()
|
).rstrip(b'=').decode()
|
||||||
|
body = base64.urlsafe_b64encode(
|
||||||
|
json.dumps(payload).encode()
|
||||||
|
).rstrip(b'=').decode()
|
||||||
msg = f"{header}.{body}".encode()
|
msg = f"{header}.{body}".encode()
|
||||||
sig = hmac.new(_SECRET, msg, hashlib.sha256).digest()
|
sig = hmac.new(_SECRET, msg, hashlib.sha256).digest()
|
||||||
return f"{header}.{body}.{base64.urlsafe_b64encode(sig).rstrip(b'=').decode()}"
|
return f"{header}.{body}.{base64.urlsafe_b64encode(sig).rstrip(b'=').decode()}"
|
||||||
@@ -53,13 +55,22 @@ def verify_jwt(token: str) -> Optional[dict]:
|
|||||||
header, body, sig = parts
|
header, body, sig = parts
|
||||||
msg = f"{header}.{body}".encode()
|
msg = f"{header}.{body}".encode()
|
||||||
expected = hmac.new(_SECRET, msg, hashlib.sha256).digest()
|
expected = hmac.new(_SECRET, msg, hashlib.sha256).digest()
|
||||||
if not hmac.compare_digest(base64.urlsafe_b64decode(sig + '==='[:3]), expected):
|
|
||||||
|
# Исправлено: правильный паддинг для base64url
|
||||||
|
sig_padded = sig + '=' * (4 - len(sig) % 4)
|
||||||
|
if not hmac.compare_digest(
|
||||||
|
base64.urlsafe_b64decode(sig_padded),
|
||||||
|
expected
|
||||||
|
):
|
||||||
return None
|
return None
|
||||||
payload = json.loads(base64.urlsafe_b64decode(body + '==='[:3]))
|
|
||||||
|
body_padded = body + '=' * (4 - len(body) % 4)
|
||||||
|
payload = json.loads(base64.urlsafe_b64decode(body_padded))
|
||||||
|
|
||||||
if payload.get("exp", 0) < time.time():
|
if payload.get("exp", 0) < time.time():
|
||||||
return None
|
return None
|
||||||
return payload
|
return payload
|
||||||
except:
|
except Exception:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# ====================== БАЗА ДАННЫХ ======================
|
# ====================== БАЗА ДАННЫХ ======================
|
||||||
@@ -72,7 +83,7 @@ def init_db():
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
conn.executescript("""
|
conn.executescript("""
|
||||||
CREATE TABLE IF NOT EXISTS users (
|
CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
username TEXT UNIQUE COLLATE NOCASE,
|
username TEXT UNIQUE COLLATE NOCASE,
|
||||||
password_hash TEXT NOT NULL,
|
password_hash TEXT NOT NULL,
|
||||||
uuid TEXT UNIQUE NOT NULL,
|
uuid TEXT UNIQUE NOT NULL,
|
||||||
@@ -81,16 +92,17 @@ def init_db():
|
|||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS passes (
|
CREATE TABLE IF NOT EXISTS passes (
|
||||||
code TEXT PRIMARY KEY, -- ZERN-XXXXXX
|
code TEXT PRIMARY KEY,
|
||||||
is_used BOOLEAN DEFAULT 0,
|
owner TEXT,
|
||||||
|
is_active BOOLEAN DEFAULT 1,
|
||||||
activated_by INTEGER REFERENCES users(id),
|
activated_by INTEGER REFERENCES users(id),
|
||||||
activated_at REAL,
|
activated_at REAL,
|
||||||
expires_at REAL, -- NULL = без срока
|
expires_at REAL,
|
||||||
max_uses INTEGER DEFAULT 1, -- пока 1, можно больше
|
max_uses INTEGER DEFAULT 1,
|
||||||
uses INTEGER DEFAULT 0
|
uses INTEGER DEFAULT 0
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TABLE IF NOT EXISTS user_passes ( -- Связь пользователь-пасс
|
CREATE TABLE IF NOT EXISTS user_passes (
|
||||||
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
user_id INTEGER REFERENCES users(id) ON DELETE CASCADE,
|
||||||
pass_code TEXT REFERENCES passes(code),
|
pass_code TEXT REFERENCES passes(code),
|
||||||
activated_at REAL NOT NULL,
|
activated_at REAL NOT NULL,
|
||||||
@@ -111,15 +123,25 @@ def init_db():
|
|||||||
# ====================== ХЕЛПЕРЫ ======================
|
# ====================== ХЕЛПЕРЫ ======================
|
||||||
def hash_password(password: str) -> str:
|
def hash_password(password: str) -> str:
|
||||||
salt = secrets.token_hex(16)
|
salt = secrets.token_hex(16)
|
||||||
hash_obj = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 300000)
|
hash_obj = hashlib.pbkdf2_hmac(
|
||||||
|
'sha256',
|
||||||
|
password.encode(),
|
||||||
|
salt.encode(),
|
||||||
|
300000
|
||||||
|
)
|
||||||
return f"{salt}${hash_obj.hex()}"
|
return f"{salt}${hash_obj.hex()}"
|
||||||
|
|
||||||
def verify_password(password: str, stored: str) -> bool:
|
def verify_password(password: str, stored: str) -> bool:
|
||||||
try:
|
try:
|
||||||
salt, stored_hash = stored.split('$')
|
salt, stored_hash = stored.split('$')
|
||||||
hash_obj = hashlib.pbkdf2_hmac('sha256', password.encode(), salt.encode(), 300000)
|
hash_obj = hashlib.pbkdf2_hmac(
|
||||||
|
'sha256',
|
||||||
|
password.encode(),
|
||||||
|
salt.encode(),
|
||||||
|
300000
|
||||||
|
)
|
||||||
return hmac.compare_digest(hash_obj.hex(), stored_hash)
|
return hmac.compare_digest(hash_obj.hex(), stored_hash)
|
||||||
except:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def generate_uuid() -> str:
|
def generate_uuid() -> str:
|
||||||
@@ -145,58 +167,6 @@ class TokenResponse(BaseModel):
|
|||||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
router = APIRouter(prefix="/auth", tags=["auth"])
|
||||||
bearer = HTTPBearer(auto_error=False)
|
bearer = HTTPBearer(auto_error=False)
|
||||||
|
|
||||||
@router.post("/register", response_model=TokenResponse)
|
|
||||||
async def register(body: RegisterRequest, request: Request):
|
|
||||||
conn = get_db()
|
|
||||||
try:
|
|
||||||
if conn.execute("SELECT 1 FROM users WHERE username = ? COLLATE NOCASE", (body.username,)).fetchone():
|
|
||||||
raise HTTPException(status_code=409, detail="Имя пользователя уже занято")
|
|
||||||
|
|
||||||
uuid = generate_uuid()
|
|
||||||
pw_hash = hash_password(body.password)
|
|
||||||
now = time.time()
|
|
||||||
|
|
||||||
conn.execute(
|
|
||||||
"INSERT INTO users (username, password_hash, uuid, created_at) VALUES (?, ?, ?, ?)",
|
|
||||||
(body.username, pw_hash, uuid, now)
|
|
||||||
)
|
|
||||||
user_id = conn.lastrowid
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
# Вызываем функцию и явно преобразуем в dict, чтобы избежать проблем сериализации
|
|
||||||
tokens = _issue_tokens(conn, user_id, body.username, uuid)
|
|
||||||
return tokens.model_dump() if hasattr(tokens, "model_dump") else tokens
|
|
||||||
|
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
|
||||||
logger.error("Register error", exc_info=True)
|
|
||||||
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login", response_model=TokenResponse)
|
|
||||||
async def login(body: LoginRequest, request: Request):
|
|
||||||
conn = get_db()
|
|
||||||
try:
|
|
||||||
row = conn.execute(
|
|
||||||
"SELECT id, username, password_hash, uuid FROM users WHERE username = ? COLLATE NOCASE",
|
|
||||||
(body.username,)
|
|
||||||
).fetchone()
|
|
||||||
|
|
||||||
if not row or not verify_password(body.password, row["password_hash"]):
|
|
||||||
raise HTTPException(401, "Неверное имя пользователя или пароль")
|
|
||||||
|
|
||||||
conn.execute("UPDATE users SET last_login = ? WHERE id = ?", (time.time(), row["id"]))
|
|
||||||
conn.commit()
|
|
||||||
|
|
||||||
return _issue_tokens(conn, row["id"], row["username"], row["uuid"])
|
|
||||||
finally:
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def _issue_tokens(conn, user_id: int, username: str, uuid: str) -> TokenResponse:
|
def _issue_tokens(conn, user_id: int, username: str, uuid: str) -> TokenResponse:
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
@@ -216,7 +186,6 @@ def _issue_tokens(conn, user_id: int, username: str, uuid: str) -> TokenResponse
|
|||||||
|
|
||||||
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
|
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
|
||||||
|
|
||||||
# Удаляем старые refresh-токены пользователя
|
|
||||||
conn.execute("DELETE FROM refresh_tokens WHERE user_id = ?", (user_id,))
|
conn.execute("DELETE FROM refresh_tokens WHERE user_id = ?", (user_id,))
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)",
|
"INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES (?, ?, ?)",
|
||||||
@@ -232,8 +201,65 @@ def _issue_tokens(conn, user_id: int, username: str, uuid: str) -> TokenResponse
|
|||||||
uuid=uuid
|
uuid=uuid
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@router.post("/register", response_model=TokenResponse)
|
||||||
|
async def register(body: RegisterRequest, request: Request):
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
existing = conn.execute(
|
||||||
|
"SELECT 1 FROM users WHERE username = ? COLLATE NOCASE",
|
||||||
|
(body.username,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if existing:
|
||||||
|
raise HTTPException(status_code=409, detail="Имя пользователя уже занято")
|
||||||
|
|
||||||
|
uuid = generate_uuid()
|
||||||
|
pw_hash = hash_password(body.password)
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
cursor = conn.execute(
|
||||||
|
"INSERT INTO users (username, password_hash, uuid, created_at) VALUES (?, ?, ?, ?)",
|
||||||
|
(body.username, pw_hash, uuid, now)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
user_id = cursor.lastrowid
|
||||||
|
tokens = _issue_tokens(conn, user_id, body.username, uuid)
|
||||||
|
|
||||||
|
logger.info("User registered", username=body.username, user_id=user_id)
|
||||||
|
return tokens
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Register error", exc_info=True)
|
||||||
|
raise HTTPException(status_code=500, detail=f"Internal server error: {str(e)}")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
@router.post("/login", response_model=TokenResponse)
|
||||||
|
async def login(body: LoginRequest, request: Request):
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
row = conn.execute(
|
||||||
|
"SELECT id, username, password_hash, uuid FROM users WHERE username = ? COLLATE NOCASE",
|
||||||
|
(body.username,)
|
||||||
|
).fetchone()
|
||||||
|
|
||||||
|
if not row or not verify_password(body.password, row["password_hash"]):
|
||||||
|
raise HTTPException(401, "Неверное имя пользователя или пароль")
|
||||||
|
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE users SET last_login = ? WHERE id = ?",
|
||||||
|
(time.time(), row["id"])
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
logger.info("User logged in", username=body.username, user_id=row["id"])
|
||||||
|
return _issue_tokens(conn, row["id"], row["username"], row["uuid"])
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
# Добавь этот эндпоинт, он используется в tryRefresh()
|
|
||||||
@router.post("/refresh")
|
@router.post("/refresh")
|
||||||
async def refresh(body: dict):
|
async def refresh(body: dict):
|
||||||
refresh_token = body.get("refresh_token")
|
refresh_token = body.get("refresh_token")
|
||||||
@@ -256,7 +282,8 @@ async def refresh(body: dict):
|
|||||||
raise HTTPException(401, "Refresh token истёк или недействителен")
|
raise HTTPException(401, "Refresh token истёк или недействителен")
|
||||||
|
|
||||||
user_row = conn.execute(
|
user_row = conn.execute(
|
||||||
"SELECT id, username, uuid FROM users WHERE id = ?", (row["user_id"],)
|
"SELECT id, username, uuid FROM users WHERE id = ?",
|
||||||
|
(row["user_id"],)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if not user_row:
|
if not user_row:
|
||||||
@@ -266,82 +293,106 @@ async def refresh(body: dict):
|
|||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
class ActivatePassRequest(BaseModel):
|
@router.post("/logout")
|
||||||
pass_code: str = Field(..., min_length=8, max_length=20, pattern=r"^ZERN-[A-Z0-9]+$")
|
async def logout(body: dict):
|
||||||
|
refresh_token = body.get("refresh_token")
|
||||||
|
if refresh_token:
|
||||||
|
conn = get_db()
|
||||||
|
try:
|
||||||
|
token_hash = hashlib.sha256(refresh_token.encode()).hexdigest()
|
||||||
|
conn.execute(
|
||||||
|
"DELETE FROM refresh_tokens WHERE token_hash = ?",
|
||||||
|
(token_hash,)
|
||||||
|
)
|
||||||
|
conn.commit()
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return {"success": True}
|
||||||
|
|
||||||
class PassInfo(BaseModel):
|
# ====================== ПРОХОДКИ ======================
|
||||||
code: str
|
|
||||||
expires_at: Optional[float] = None
|
class ActivatePassRequest(BaseModel):
|
||||||
is_active: bool
|
pass_code: str = Field(..., min_length=8, max_length=20)
|
||||||
|
|
||||||
@router.post("/pass/activate")
|
@router.post("/pass/activate")
|
||||||
async def activate_pass(body: ActivatePassRequest, credentials: HTTPAuthorizationCredentials = Depends(bearer)):
|
async def activate_pass_endpoint(
|
||||||
token = credentials.credentials if credentials else None
|
body: ActivatePassRequest,
|
||||||
if not token:
|
credentials: HTTPAuthorizationCredentials = Depends(bearer)
|
||||||
|
):
|
||||||
|
if not credentials:
|
||||||
raise HTTPException(401, "Требуется авторизация")
|
raise HTTPException(401, "Требуется авторизация")
|
||||||
|
|
||||||
payload = verify_jwt(token)
|
payload = verify_jwt(credentials.credentials)
|
||||||
if not payload or payload.get("type") != "access":
|
if not payload or payload.get("type") != "access":
|
||||||
raise HTTPException(401, "Недействительный токен")
|
raise HTTPException(401, "Недействительный токен")
|
||||||
|
|
||||||
user_id = payload["sub"]
|
user_id = payload["sub"]
|
||||||
|
username = payload["username"]
|
||||||
|
pass_code = body.pass_code.upper().strip()
|
||||||
|
|
||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
# Проверяем существование и доступность пасса
|
|
||||||
pass_row = conn.execute(
|
pass_row = conn.execute(
|
||||||
"SELECT code, expires_at, uses, max_uses FROM passes WHERE code = ?",
|
"SELECT code, expires_at, uses, max_uses, owner FROM passes WHERE code = ?",
|
||||||
(body.pass_code.upper(),)
|
(pass_code,)
|
||||||
).fetchone()
|
).fetchone()
|
||||||
|
|
||||||
if not pass_row:
|
if not pass_row:
|
||||||
raise HTTPException(404, "Проходка не найдена")
|
raise HTTPException(404, "Проходка не найдена")
|
||||||
|
|
||||||
|
# Проверка срока
|
||||||
if pass_row["expires_at"] and pass_row["expires_at"] < time.time():
|
if pass_row["expires_at"] and pass_row["expires_at"] < time.time():
|
||||||
raise HTTPException(410, "Проходка истекла")
|
raise HTTPException(410, "Проходка истекла")
|
||||||
|
|
||||||
|
# Проверка лимита использований
|
||||||
if pass_row["uses"] >= pass_row["max_uses"]:
|
if pass_row["uses"] >= pass_row["max_uses"]:
|
||||||
raise HTTPException(410, "Проходка уже использована")
|
raise HTTPException(410, "Проходка уже использована")
|
||||||
|
|
||||||
# Проверяем, не активировал ли уже этот пользователь
|
# Проверка владельца
|
||||||
already = conn.execute(
|
if pass_row["owner"] is not None:
|
||||||
"SELECT 1 FROM user_passes WHERE user_id = ? AND pass_code = ?",
|
if pass_row["owner"] != username:
|
||||||
(user_id, body.pass_code.upper())
|
raise HTTPException(409, "Проходка уже активирована другим пользователем")
|
||||||
).fetchone()
|
|
||||||
|
|
||||||
if already:
|
# Уже активирована этим пользователем
|
||||||
raise HTTPException(409, "Эта проходка уже активирована на вашем аккаунте")
|
return {"success": True, "message": "Проходка уже активирована на вашем аккаунте"}
|
||||||
|
|
||||||
now = time.time()
|
now = time.time()
|
||||||
|
|
||||||
# Активируем
|
# Активация
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"INSERT INTO user_passes (user_id, pass_code, activated_at) VALUES (?, ?, ?)",
|
"INSERT INTO user_passes (user_id, pass_code, activated_at) VALUES (?, ?, ?)",
|
||||||
(user_id, body.pass_code.upper(), now)
|
(user_id, pass_code, now)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Увеличиваем счётчик использований
|
|
||||||
conn.execute(
|
conn.execute(
|
||||||
"UPDATE passes SET uses = uses + 1, activated_by = ?, activated_at = ? WHERE code = ?",
|
"""UPDATE passes
|
||||||
(user_id, now, body.pass_code.upper())
|
SET uses = uses + 1,
|
||||||
|
owner = ?,
|
||||||
|
activated_by = ?,
|
||||||
|
activated_at = ?
|
||||||
|
WHERE code = ?""",
|
||||||
|
(username, user_id, now, pass_code)
|
||||||
)
|
)
|
||||||
|
|
||||||
conn.commit()
|
conn.commit()
|
||||||
|
|
||||||
logger.info("Pass activated", user_id=user_id, pass_code=body.pass_code)
|
logger.info("Pass activated", user_id=user_id, username=username, pass_code=pass_code)
|
||||||
return {"success": True, "message": "Проходка успешно активирована!"}
|
return {"success": True, "message": "Проходка успешно активирована!"}
|
||||||
|
|
||||||
|
except HTTPException:
|
||||||
|
raise
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Pass activation error", exc_info=True)
|
||||||
|
raise HTTPException(500, f"Ошибка сервера: {str(e)}")
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/pass/my")
|
@router.get("/pass/my")
|
||||||
async def get_my_passes(credentials: HTTPAuthorizationCredentials = Depends(bearer)):
|
async def get_my_passes(credentials: HTTPAuthorizationCredentials = Depends(bearer)):
|
||||||
token = credentials.credentials if credentials else None
|
if not credentials:
|
||||||
if not token:
|
|
||||||
raise HTTPException(401, "Требуется авторизация")
|
raise HTTPException(401, "Требуется авторизация")
|
||||||
|
|
||||||
payload = verify_jwt(token)
|
payload = verify_jwt(credentials.credentials)
|
||||||
if not payload:
|
if not payload:
|
||||||
raise HTTPException(401, "Недействительный токен")
|
raise HTTPException(401, "Недействительный токен")
|
||||||
|
|
||||||
@@ -350,7 +401,7 @@ async def get_my_passes(credentials: HTTPAuthorizationCredentials = Depends(bear
|
|||||||
conn = get_db()
|
conn = get_db()
|
||||||
try:
|
try:
|
||||||
rows = conn.execute("""
|
rows = conn.execute("""
|
||||||
SELECT p.code, p.expires_at, up.activated_at
|
SELECT p.code, p.expires_at, p.is_active, up.activated_at
|
||||||
FROM user_passes up
|
FROM user_passes up
|
||||||
JOIN passes p ON up.pass_code = p.code
|
JOIN passes p ON up.pass_code = p.code
|
||||||
WHERE up.user_id = ?
|
WHERE up.user_id = ?
|
||||||
@@ -360,7 +411,7 @@ async def get_my_passes(credentials: HTTPAuthorizationCredentials = Depends(bear
|
|||||||
now = time.time()
|
now = time.time()
|
||||||
for row in rows:
|
for row in rows:
|
||||||
expires = row["expires_at"]
|
expires = row["expires_at"]
|
||||||
is_active = expires is None or expires > now
|
is_active = row["is_active"] and (expires is None or expires > now)
|
||||||
passes.append({
|
passes.append({
|
||||||
"code": row["code"],
|
"code": row["code"],
|
||||||
"activated_at": row["activated_at"],
|
"activated_at": row["activated_at"],
|
||||||
@@ -368,6 +419,9 @@ async def get_my_passes(credentials: HTTPAuthorizationCredentials = Depends(bear
|
|||||||
"is_active": is_active
|
"is_active": is_active
|
||||||
})
|
})
|
||||||
|
|
||||||
return {"passes": passes}
|
return {
|
||||||
|
"passes": passes,
|
||||||
|
"has_active": any(p["is_active"] for p in passes)
|
||||||
|
}
|
||||||
finally:
|
finally:
|
||||||
conn.close()
|
conn.close()
|
||||||
@@ -20,7 +20,6 @@ import base64
|
|||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
|
|
||||||
from auth import router as auth_router, init_db, verify_jwt
|
from auth import router as auth_router, init_db, verify_jwt
|
||||||
from pass_manager import activate_pass, has_active_pass, get_user_passes
|
|
||||||
|
|
||||||
logger = structlog.get_logger(__name__)
|
logger = structlog.get_logger(__name__)
|
||||||
|
|
||||||
@@ -826,49 +825,6 @@ async def proxy_status():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@app.post("/auth/pass/activate")
|
|
||||||
async def api_activate_pass(request: Request):
|
|
||||||
try:
|
|
||||||
body = await request.json()
|
|
||||||
pass_code = body.get("pass_code")
|
|
||||||
if not pass_code:
|
|
||||||
raise HTTPException(400, "pass_code обязателен")
|
|
||||||
except:
|
|
||||||
raise HTTPException(400, "Неверный JSON")
|
|
||||||
|
|
||||||
# Получаем текущего пользователя из токена
|
|
||||||
auth_header = request.headers.get("Authorization")
|
|
||||||
if not auth_header or not auth_header.startswith("Bearer "):
|
|
||||||
raise HTTPException(401, "Требуется авторизация")
|
|
||||||
|
|
||||||
token = auth_header.split(" ")[1]
|
|
||||||
payload = verify_jwt(token) # функция из auth.py
|
|
||||||
if not payload:
|
|
||||||
raise HTTPException(401, "Недействительный токен")
|
|
||||||
|
|
||||||
result = activate_pass(pass_code, payload["username"], payload["sub"])
|
|
||||||
|
|
||||||
if result["success"]:
|
|
||||||
return result
|
|
||||||
else:
|
|
||||||
raise HTTPException(400, result["error"])
|
|
||||||
|
|
||||||
|
|
||||||
@app.get("/auth/pass/my")
|
|
||||||
async def api_my_passes(request: Request):
|
|
||||||
auth_header = request.headers.get("Authorization")
|
|
||||||
if not auth_header or not auth_header.startswith("Bearer "):
|
|
||||||
raise HTTPException(401, "Требуется авторизация")
|
|
||||||
|
|
||||||
token = auth_header.split(" ")[1]
|
|
||||||
payload = verify_jwt(token)
|
|
||||||
if not payload:
|
|
||||||
raise HTTPException(401, "Недействительный токен")
|
|
||||||
|
|
||||||
passes = get_user_passes(payload["username"])
|
|
||||||
return {"passes": passes, "has_active": any(p["is_active"] for p in passes)}
|
|
||||||
|
|
||||||
|
|
||||||
@app.exception_handler(Exception)
|
@app.exception_handler(Exception)
|
||||||
async def global_exception_handler(request: Request, exc: Exception):
|
async def global_exception_handler(request: Request, exc: Exception):
|
||||||
logger.error("Unhandled exception", exc_info=True)
|
logger.error("Unhandled exception", exc_info=True)
|
||||||
|
|||||||
Reference in New Issue
Block a user