Загрузить файлы в «/»
This commit is contained in:
10
config.py
Normal file
10
config.py
Normal file
@@ -0,0 +1,10 @@
|
||||
import os
|
||||
import secrets
|
||||
|
||||
START_VIA_SCRIPT = os.getenv("ROBO_OPS_VIA_START_SH") == "1"
|
||||
|
||||
SECRET_KEY = os.getenv("SECRET_KEY", secrets.token_hex(32))
|
||||
SESSION_COOKIE = "robo_ops_session"
|
||||
SESSION_TTL_HOURS = 24
|
||||
EXTERNAL_API_KEY = os.getenv("EXTERNAL_API_KEY", "robo-ops-external-demo-key-2026")
|
||||
DATABASE_PATH = os.getenv("DATABASE_PATH", "data/robo_ops.db")
|
||||
312
database.py
Normal file
312
database.py
Normal file
@@ -0,0 +1,312 @@
|
||||
import json
|
||||
import sqlite3
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
from app.config import DATABASE_PATH
|
||||
|
||||
ROBOTS_DEFAULT = [
|
||||
{
|
||||
"id": "RB-001", "number": 1, "name": "Альфа-7", "type": "Промышленный манипулятор",
|
||||
"status": "online", "battery": 87, "speed": 1.2, "maxSpeed": 2.4, "baseTemp": 32,
|
||||
"payload": 15, "temperature": 42, "uptime": "142ч", "firmware": "v3.2.1",
|
||||
"mode": "auto", "speedSetting": 50, "sensitivity": 70, "emergency": False, "nightMode": False,
|
||||
},
|
||||
{
|
||||
"id": "RB-002", "number": 2, "name": "Бета-3", "type": "Мобильный транспортёр",
|
||||
"status": "idle", "battery": 64, "speed": 0.8, "maxSpeed": 1.6, "baseTemp": 30,
|
||||
"payload": 50, "temperature": 38, "uptime": "89ч", "firmware": "v2.8.0",
|
||||
"mode": "assist", "speedSetting": 35, "sensitivity": 55, "emergency": False, "nightMode": True,
|
||||
},
|
||||
{
|
||||
"id": "RB-003", "number": 3, "name": "Гамма-X", "type": "Сервисный ассистент",
|
||||
"status": "online", "battery": 95, "speed": 0.5, "maxSpeed": 1.0, "baseTemp": 28,
|
||||
"payload": 5, "temperature": 35, "uptime": "210ч", "firmware": "v4.0.2",
|
||||
"mode": "manual", "speedSetting": 60, "sensitivity": 80, "emergency": False, "nightMode": False,
|
||||
},
|
||||
{
|
||||
"id": "RB-004", "number": 4, "name": "Дельта-9", "type": "Исследовательский дрон",
|
||||
"status": "offline", "battery": 12, "speed": 0, "maxSpeed": 5.0, "baseTemp": 22,
|
||||
"payload": 2, "temperature": 28, "uptime": "0ч", "firmware": "v1.9.5",
|
||||
"mode": "standby", "speedSetting": 20, "sensitivity": 40, "emergency": True, "nightMode": False,
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def utcnow() -> datetime:
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def get_db():
|
||||
Path(DATABASE_PATH).parent.mkdir(parents=True, exist_ok=True)
|
||||
conn = sqlite3.connect(DATABASE_PATH)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
try:
|
||||
yield conn
|
||||
conn.commit()
|
||||
except Exception:
|
||||
conn.rollback()
|
||||
raise
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
def init_db():
|
||||
with get_db() as conn:
|
||||
conn.executescript("""
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
email TEXT NOT NULL UNIQUE COLLATE NOCASE,
|
||||
password_hash TEXT NOT NULL,
|
||||
firstname TEXT DEFAULT '',
|
||||
lastname TEXT DEFAULT '',
|
||||
patronymic TEXT DEFAULT '',
|
||||
age INTEGER,
|
||||
phone TEXT DEFAULT '',
|
||||
rating REAL NOT NULL DEFAULT 0,
|
||||
points INTEGER NOT NULL DEFAULT 0,
|
||||
selected_robot_id TEXT DEFAULT 'RB-001',
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS sessions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
token_hash TEXT NOT NULL UNIQUE,
|
||||
created_at TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
user_agent TEXT DEFAULT '',
|
||||
ip_address TEXT DEFAULT '',
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS robot_settings (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
robot_id TEXT NOT NULL,
|
||||
settings_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL,
|
||||
UNIQUE(user_id, robot_id),
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS external_data (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER,
|
||||
source_app TEXT NOT NULL,
|
||||
data_key TEXT NOT NULL,
|
||||
data_value TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token_hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_sessions_user ON sessions(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_external_user ON external_data(user_id);
|
||||
""")
|
||||
|
||||
|
||||
def row_to_dict(row: sqlite3.Row) -> dict:
|
||||
return dict(row) if row else None
|
||||
|
||||
|
||||
def create_user(email: str, password_hash: str) -> dict:
|
||||
now = utcnow().isoformat()
|
||||
with get_db() as conn:
|
||||
cur = conn.execute(
|
||||
"INSERT INTO users (email, password_hash, created_at) VALUES (?, ?, ?)",
|
||||
(email.lower().strip(), password_hash, now),
|
||||
)
|
||||
user_id = cur.lastrowid
|
||||
for robot in ROBOTS_DEFAULT:
|
||||
conn.execute(
|
||||
"INSERT INTO robot_settings (user_id, robot_id, settings_json, updated_at) VALUES (?, ?, ?, ?)",
|
||||
(user_id, robot["id"], json.dumps(robot, ensure_ascii=False), now),
|
||||
)
|
||||
row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def get_user_by_email(email: str) -> dict | None:
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM users WHERE email = ? COLLATE NOCASE",
|
||||
(email.lower().strip(),),
|
||||
).fetchone()
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def get_user_by_id(user_id: int) -> dict | None:
|
||||
with get_db() as conn:
|
||||
row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def update_user_profile(user_id: int, data: dict) -> dict:
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"""UPDATE users SET firstname=?, lastname=?, patronymic=?, age=?, phone=?
|
||||
WHERE id=?""",
|
||||
(
|
||||
data.get("firstname", ""),
|
||||
data.get("lastname", ""),
|
||||
data.get("patronymic", ""),
|
||||
data.get("age"),
|
||||
data.get("phone", ""),
|
||||
user_id,
|
||||
),
|
||||
)
|
||||
row = conn.execute("SELECT * FROM users WHERE id = ?", (user_id,)).fetchone()
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def create_session(user_id: int, token_hash: str, user_agent: str, ip_address: str, ttl_hours: int) -> dict:
|
||||
now = utcnow()
|
||||
expires = now + timedelta(hours=ttl_hours)
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO sessions (user_id, token_hash, created_at, expires_at, user_agent, ip_address)
|
||||
VALUES (?, ?, ?, ?, ?, ?)""",
|
||||
(user_id, token_hash, now.isoformat(), expires.isoformat(), user_agent, ip_address),
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM sessions WHERE token_hash = ?", (token_hash,)
|
||||
).fetchone()
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def get_session_by_token_hash(token_hash: str) -> dict | None:
|
||||
with get_db() as conn:
|
||||
row = conn.execute(
|
||||
"SELECT * FROM sessions WHERE token_hash = ? AND is_active = 1",
|
||||
(token_hash,),
|
||||
).fetchone()
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def deactivate_session(token_hash: str) -> None:
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"UPDATE sessions SET is_active = 0 WHERE token_hash = ?",
|
||||
(token_hash,),
|
||||
)
|
||||
|
||||
|
||||
def get_active_sessions(user_id: int | None = None) -> list[dict]:
|
||||
now = utcnow().isoformat()
|
||||
with get_db() as conn:
|
||||
if user_id is not None:
|
||||
rows = conn.execute(
|
||||
"""SELECT s.*, u.email FROM sessions s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.is_active = 1 AND s.expires_at > ? AND s.user_id = ?
|
||||
ORDER BY s.created_at DESC""",
|
||||
(now, user_id),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"""SELECT s.*, u.email FROM sessions s
|
||||
JOIN users u ON u.id = s.user_id
|
||||
WHERE s.is_active = 1 AND s.expires_at > ?
|
||||
ORDER BY s.created_at DESC""",
|
||||
(now,),
|
||||
).fetchall()
|
||||
return [row_to_dict(r) for r in rows]
|
||||
|
||||
|
||||
def get_user_robots(user_id: int) -> list[dict]:
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT robot_id, settings_json FROM robot_settings WHERE user_id = ? ORDER BY robot_id",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
if not rows:
|
||||
now = utcnow().isoformat()
|
||||
for robot in ROBOTS_DEFAULT:
|
||||
conn.execute(
|
||||
"INSERT INTO robot_settings (user_id, robot_id, settings_json, updated_at) VALUES (?, ?, ?, ?)",
|
||||
(user_id, robot["id"], json.dumps(robot, ensure_ascii=False), now),
|
||||
)
|
||||
rows = conn.execute(
|
||||
"SELECT robot_id, settings_json FROM robot_settings WHERE user_id = ? ORDER BY robot_id",
|
||||
(user_id,),
|
||||
).fetchall()
|
||||
return [json.loads(r["settings_json"]) for r in rows]
|
||||
|
||||
|
||||
def update_robot_settings(user_id: int, robot_id: str, settings: dict) -> dict:
|
||||
now = utcnow().isoformat()
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"""INSERT INTO robot_settings (user_id, robot_id, settings_json, updated_at)
|
||||
VALUES (?, ?, ?, ?)
|
||||
ON CONFLICT(user_id, robot_id) DO UPDATE SET
|
||||
settings_json = excluded.settings_json, updated_at = excluded.updated_at""",
|
||||
(user_id, robot_id, json.dumps(settings, ensure_ascii=False), now),
|
||||
)
|
||||
return settings
|
||||
|
||||
|
||||
def set_selected_robot(user_id: int, robot_id: str) -> None:
|
||||
with get_db() as conn:
|
||||
conn.execute(
|
||||
"UPDATE users SET selected_robot_id = ? WHERE id = ?",
|
||||
(robot_id, user_id),
|
||||
)
|
||||
|
||||
|
||||
def add_external_data(user_id: int | None, source_app: str, data_key: str, data_value: dict) -> dict:
|
||||
now = utcnow().isoformat()
|
||||
with get_db() as conn:
|
||||
cur = conn.execute(
|
||||
"""INSERT INTO external_data (user_id, source_app, data_key, data_value, created_at)
|
||||
VALUES (?, ?, ?, ?, ?)""",
|
||||
(user_id, source_app, data_key, json.dumps(data_value, ensure_ascii=False), now),
|
||||
)
|
||||
if user_id and "points" in data_value:
|
||||
points = int(data_value["points"])
|
||||
conn.execute(
|
||||
"UPDATE users SET points = points + ? WHERE id = ?",
|
||||
(points, user_id),
|
||||
)
|
||||
if user_id and "rating" in data_value:
|
||||
conn.execute(
|
||||
"UPDATE users SET rating = ? WHERE id = ?",
|
||||
(float(data_value["rating"]), user_id),
|
||||
)
|
||||
row = conn.execute(
|
||||
"SELECT * FROM external_data WHERE id = ?", (cur.lastrowid,)
|
||||
).fetchone()
|
||||
return row_to_dict(row)
|
||||
|
||||
|
||||
def get_external_data(user_id: int | None = None, limit: int = 100) -> list[dict]:
|
||||
with get_db() as conn:
|
||||
if user_id is not None:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM external_data WHERE user_id = ? ORDER BY created_at DESC LIMIT ?",
|
||||
(user_id, limit),
|
||||
).fetchall()
|
||||
else:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM external_data ORDER BY created_at DESC LIMIT ?",
|
||||
(limit,),
|
||||
).fetchall()
|
||||
result = []
|
||||
for r in rows:
|
||||
item = row_to_dict(r)
|
||||
item["data_value"] = json.loads(item["data_value"])
|
||||
result.append(item)
|
||||
return result
|
||||
|
||||
|
||||
def list_users_public() -> list[dict]:
|
||||
with get_db() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT id, email, firstname, lastname, rating, points, created_at FROM users ORDER BY id"
|
||||
).fetchall()
|
||||
return [row_to_dict(r) for r in rows]
|
||||
280
main.py
Normal file
280
main.py
Normal file
@@ -0,0 +1,280 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from fastapi import Cookie, Depends, FastAPI, HTTPException, Request, Response
|
||||
from fastapi.responses import FileResponse
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
|
||||
from app import database as db
|
||||
from app.config import EXTERNAL_API_KEY, SESSION_COOKIE, SESSION_TTL_HOURS, START_VIA_SCRIPT
|
||||
from app.database import init_db
|
||||
from app.schemas import (
|
||||
ExternalDataWrite,
|
||||
LoginRequest,
|
||||
ProfileUpdate,
|
||||
RegisterRequest,
|
||||
RobotSettingsUpdate,
|
||||
RobotsBulkUpdate,
|
||||
)
|
||||
from app.security import (
|
||||
generate_session_token,
|
||||
hash_password,
|
||||
hash_token,
|
||||
verify_password,
|
||||
)
|
||||
|
||||
app = FastAPI(title="ROBO-OPS", version="2.0.0")
|
||||
|
||||
STATIC_DIR = __import__("pathlib").Path(__file__).resolve().parent.parent / "static"
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
def on_startup():
|
||||
if not START_VIA_SCRIPT:
|
||||
raise RuntimeError(
|
||||
"Приложение запускается только через ./start.sh\n"
|
||||
"См. README.md — раздел «Запуск приложения»."
|
||||
)
|
||||
init_db()
|
||||
|
||||
|
||||
def user_public(user: dict) -> dict:
|
||||
return {
|
||||
"id": user["id"],
|
||||
"email": user["email"],
|
||||
"firstname": user["firstname"],
|
||||
"lastname": user["lastname"],
|
||||
"patronymic": user["patronymic"],
|
||||
"age": user["age"],
|
||||
"phone": user["phone"],
|
||||
"rating": user["rating"],
|
||||
"points": user["points"],
|
||||
"selected_robot_id": user["selected_robot_id"],
|
||||
}
|
||||
|
||||
|
||||
def session_public(session: dict) -> dict:
|
||||
return {
|
||||
"id": session["id"],
|
||||
"user_id": session["user_id"],
|
||||
"email": session.get("email", ""),
|
||||
"created_at": session["created_at"],
|
||||
"expires_at": session["expires_at"],
|
||||
"user_agent": session["user_agent"],
|
||||
"ip_address": session["ip_address"],
|
||||
}
|
||||
|
||||
|
||||
async def get_current_user(
|
||||
request: Request,
|
||||
session_token: str | None = Cookie(default=None, alias=SESSION_COOKIE),
|
||||
) -> dict:
|
||||
if not session_token:
|
||||
raise HTTPException(status_code=401, detail="Требуется авторизация")
|
||||
token_hash = hash_token(session_token)
|
||||
session = db.get_session_by_token_hash(token_hash)
|
||||
if not session:
|
||||
raise HTTPException(status_code=401, detail="Сессия недействительна")
|
||||
expires = datetime.fromisoformat(session["expires_at"])
|
||||
if expires.tzinfo is None:
|
||||
expires = expires.replace(tzinfo=timezone.utc)
|
||||
if expires < datetime.now(timezone.utc):
|
||||
db.deactivate_session(token_hash)
|
||||
raise HTTPException(status_code=401, detail="Сессия истекла")
|
||||
user = db.get_user_by_id(session["user_id"])
|
||||
if not user:
|
||||
raise HTTPException(status_code=401, detail="Пользователь не найден")
|
||||
return user
|
||||
|
||||
|
||||
def verify_external_api_key(request: Request) -> None:
|
||||
api_key = request.headers.get("X-API-Key")
|
||||
if not api_key or api_key != EXTERNAL_API_KEY:
|
||||
raise HTTPException(status_code=403, detail="Неверный API-ключ")
|
||||
|
||||
|
||||
# --- Auth ---
|
||||
|
||||
@app.post("/api/auth/register")
|
||||
def register(data: RegisterRequest, response: Response, request: Request):
|
||||
if db.get_user_by_email(data.email):
|
||||
raise HTTPException(status_code=400, detail="Email уже зарегистрирован")
|
||||
user = db.create_user(data.email, hash_password(data.password))
|
||||
token = generate_session_token()
|
||||
db.create_session(
|
||||
user["id"],
|
||||
hash_token(token),
|
||||
request.headers.get("user-agent", ""),
|
||||
request.client.host if request.client else "",
|
||||
SESSION_TTL_HOURS,
|
||||
)
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE,
|
||||
value=token,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=SESSION_TTL_HOURS * 3600,
|
||||
)
|
||||
return {"user": user_public(user), "message": "Регистрация успешна"}
|
||||
|
||||
|
||||
@app.post("/api/auth/login")
|
||||
def login(data: LoginRequest, response: Response, request: Request):
|
||||
user = db.get_user_by_email(data.email)
|
||||
if not user or not verify_password(data.password, user["password_hash"]):
|
||||
raise HTTPException(status_code=401, detail="Неверный email или пароль")
|
||||
token = generate_session_token()
|
||||
db.create_session(
|
||||
user["id"],
|
||||
hash_token(token),
|
||||
request.headers.get("user-agent", ""),
|
||||
request.client.host if request.client else "",
|
||||
SESSION_TTL_HOURS,
|
||||
)
|
||||
response.set_cookie(
|
||||
key=SESSION_COOKIE,
|
||||
value=token,
|
||||
httponly=True,
|
||||
samesite="lax",
|
||||
max_age=SESSION_TTL_HOURS * 3600,
|
||||
)
|
||||
return {"user": user_public(user), "message": "Вход выполнен"}
|
||||
|
||||
|
||||
@app.post("/api/auth/logout")
|
||||
def logout(
|
||||
response: Response,
|
||||
session_token: str | None = Cookie(default=None, alias=SESSION_COOKIE),
|
||||
):
|
||||
if session_token:
|
||||
db.deactivate_session(hash_token(session_token))
|
||||
response.delete_cookie(SESSION_COOKIE)
|
||||
return {"message": "Выход выполнен"}
|
||||
|
||||
|
||||
@app.get("/api/auth/me")
|
||||
def me(user: dict = Depends(get_current_user)):
|
||||
return user_public(user)
|
||||
|
||||
|
||||
# --- Profile ---
|
||||
|
||||
@app.get("/api/profile")
|
||||
def get_profile(user: dict = Depends(get_current_user)):
|
||||
return user_public(user)
|
||||
|
||||
|
||||
@app.put("/api/profile")
|
||||
def update_profile(data: ProfileUpdate, user: dict = Depends(get_current_user)):
|
||||
updated = db.update_user_profile(user["id"], data.model_dump())
|
||||
return user_public(updated)
|
||||
|
||||
|
||||
# --- Sessions ---
|
||||
|
||||
@app.get("/api/sessions")
|
||||
def my_sessions(user: dict = Depends(get_current_user)):
|
||||
sessions = db.get_active_sessions(user["id"])
|
||||
return {"sessions": [session_public(s) for s in sessions]}
|
||||
|
||||
|
||||
# --- Robots ---
|
||||
|
||||
@app.get("/api/robots")
|
||||
def get_robots(user: dict = Depends(get_current_user)):
|
||||
robots = db.get_user_robots(user["id"])
|
||||
return {
|
||||
"robots": robots,
|
||||
"selected_robot_id": user["selected_robot_id"],
|
||||
}
|
||||
|
||||
|
||||
@app.put("/api/robots")
|
||||
def save_robots(data: RobotsBulkUpdate, user: dict = Depends(get_current_user)):
|
||||
for robot in data.robots:
|
||||
robot_id = robot.get("id")
|
||||
if robot_id:
|
||||
db.update_robot_settings(user["id"], robot_id, robot)
|
||||
if data.selected_robot_id:
|
||||
db.set_selected_robot(user["id"], data.selected_robot_id)
|
||||
user = db.get_user_by_id(user["id"])
|
||||
return {
|
||||
"robots": db.get_user_robots(user["id"]),
|
||||
"selected_robot_id": user["selected_robot_id"],
|
||||
}
|
||||
|
||||
|
||||
@app.put("/api/robots/{robot_id}")
|
||||
def save_robot(robot_id: str, data: RobotSettingsUpdate, user: dict = Depends(get_current_user)):
|
||||
settings = {**data.settings, "id": robot_id}
|
||||
db.update_robot_settings(user["id"], robot_id, settings)
|
||||
return settings
|
||||
|
||||
|
||||
# --- External API ---
|
||||
|
||||
@app.get("/api/external/users")
|
||||
def external_list_users(_: None = Depends(verify_external_api_key)):
|
||||
return {"users": db.list_users_public()}
|
||||
|
||||
|
||||
@app.get("/api/external/users/{user_id}")
|
||||
def external_get_user(user_id: int, _: None = Depends(verify_external_api_key)):
|
||||
user = db.get_user_by_id(user_id)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
return user_public(user)
|
||||
|
||||
|
||||
@app.get("/api/external/users/by-email/{email}")
|
||||
def external_get_user_by_email(email: str, _: None = Depends(verify_external_api_key)):
|
||||
user = db.get_user_by_email(email)
|
||||
if not user:
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
return user_public(user)
|
||||
|
||||
|
||||
@app.get("/api/external/sessions")
|
||||
def external_sessions(
|
||||
user_id: int | None = None,
|
||||
_: None = Depends(verify_external_api_key),
|
||||
):
|
||||
sessions = db.get_active_sessions(user_id)
|
||||
return {"sessions": [session_public(s) for s in sessions]}
|
||||
|
||||
|
||||
@app.post("/api/external/data")
|
||||
def external_write_data(data: ExternalDataWrite, _: None = Depends(verify_external_api_key)):
|
||||
if data.user_id is not None and not db.get_user_by_id(data.user_id):
|
||||
raise HTTPException(status_code=404, detail="Пользователь не найден")
|
||||
record = db.add_external_data(data.user_id, data.source_app, data.data_key, data.data_value)
|
||||
record["data_value"] = __import__("json").loads(record["data_value"])
|
||||
return {"record": record, "message": "Данные записаны"}
|
||||
|
||||
|
||||
@app.get("/api/external/data")
|
||||
def external_get_data(
|
||||
user_id: int | None = None,
|
||||
limit: int = 100,
|
||||
_: None = Depends(verify_external_api_key),
|
||||
):
|
||||
return {"data": db.get_external_data(user_id, min(limit, 500))}
|
||||
|
||||
|
||||
# --- Static pages ---
|
||||
|
||||
@app.get("/")
|
||||
def index():
|
||||
return FileResponse(STATIC_DIR / "index.html")
|
||||
|
||||
|
||||
@app.get("/login")
|
||||
def login_page():
|
||||
return FileResponse(STATIC_DIR / "login.html")
|
||||
|
||||
|
||||
@app.get("/register")
|
||||
def register_page():
|
||||
return FileResponse(STATIC_DIR / "register.html")
|
||||
|
||||
|
||||
app.mount("/static", StaticFiles(directory=STATIC_DIR), name="static")
|
||||
50
schemas.py
Normal file
50
schemas.py
Normal file
@@ -0,0 +1,50 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, EmailStr, Field
|
||||
|
||||
|
||||
class RegisterRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str = Field(min_length=6, max_length=128)
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
email: EmailStr
|
||||
password: str
|
||||
|
||||
|
||||
class ProfileUpdate(BaseModel):
|
||||
firstname: str = ""
|
||||
lastname: str = ""
|
||||
patronymic: str = ""
|
||||
age: int | None = Field(default=None, ge=18, le=120)
|
||||
phone: str = ""
|
||||
|
||||
|
||||
class RobotSettingsUpdate(BaseModel):
|
||||
settings: dict[str, Any]
|
||||
|
||||
|
||||
class RobotsBulkUpdate(BaseModel):
|
||||
robots: list[dict[str, Any]]
|
||||
selected_robot_id: str | None = None
|
||||
|
||||
|
||||
class ExternalDataWrite(BaseModel):
|
||||
user_id: int | None = None
|
||||
source_app: str = Field(min_length=1, max_length=100)
|
||||
data_key: str = Field(min_length=1, max_length=100)
|
||||
data_value: dict[str, Any]
|
||||
|
||||
|
||||
class UserPublic(BaseModel):
|
||||
id: int
|
||||
email: str
|
||||
firstname: str
|
||||
lastname: str
|
||||
patronymic: str
|
||||
age: int | None
|
||||
phone: str
|
||||
rating: float
|
||||
points: int
|
||||
selected_robot_id: str
|
||||
20
security.py
Normal file
20
security.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import hashlib
|
||||
import secrets
|
||||
|
||||
import bcrypt
|
||||
|
||||
|
||||
def hash_password(password: str) -> str:
|
||||
return bcrypt.hashpw(password.encode(), bcrypt.gensalt()).decode()
|
||||
|
||||
|
||||
def verify_password(plain: str, hashed: str) -> bool:
|
||||
return bcrypt.checkpw(plain.encode(), hashed.encode())
|
||||
|
||||
|
||||
def generate_session_token() -> str:
|
||||
return secrets.token_urlsafe(32)
|
||||
|
||||
|
||||
def hash_token(token: str) -> str:
|
||||
return hashlib.sha256(token.encode()).hexdigest()
|
||||
Reference in New Issue
Block a user