lockbot 2.2.0__tar.gz → 2.3.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {lockbot-2.2.0/python/lockbot.egg-info → lockbot-2.3.0}/PKG-INFO +1 -1
- {lockbot-2.2.0 → lockbot-2.3.0}/pyproject.toml +1 -1
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/admin/router.py +37 -16
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/auth/dependencies.py +49 -1
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/auth/models.py +6 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/auth/router.py +3 -3
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/bots/router.py +15 -5
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/main.py +16 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/settings/router.py +27 -1
- {lockbot-2.2.0 → lockbot-2.3.0/python/lockbot.egg-info}/PKG-INFO +1 -1
- {lockbot-2.2.0 → lockbot-2.3.0}/LICENSE +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/MANIFEST.in +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/README.md +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/admin/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/auth/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/auth/schemas.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/bots/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/bots/encryption.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/bots/manager.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/bots/models.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/bots/schemas.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/bots/webhook_handler.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/config.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/database.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/logs/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/settings/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/backend/app/settings/models.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/base_bot.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/bot_instance.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/config.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/device_bot.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/device_usage_alert.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/device_usage_utils.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/entry.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/env.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/handler.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/i18n/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/i18n/en.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/i18n/zh.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/io.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/message_adapter.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/msg_utils.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/node_bot.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/platforms/__init__.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/platforms/infoflow.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/queue_bot.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/request.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot/core/utils.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot.egg-info/SOURCES.txt +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot.egg-info/dependency_links.txt +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot.egg-info/requires.txt +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/python/lockbot.egg-info/top_level.txt +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/setup.cfg +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/tools/create_super_admin.py +0 -0
- {lockbot-2.2.0 → lockbot-2.3.0}/tools/gen_keys.py +0 -0
|
@@ -16,7 +16,13 @@ from fastapi.responses import FileResponse, Response
|
|
|
16
16
|
from sqlalchemy.orm import Session
|
|
17
17
|
from starlette.background import BackgroundTask
|
|
18
18
|
|
|
19
|
-
from lockbot.backend.app.auth.dependencies import
|
|
19
|
+
from lockbot.backend.app.auth.dependencies import (
|
|
20
|
+
can_assign_role,
|
|
21
|
+
can_create_user_with_role,
|
|
22
|
+
can_manage_user,
|
|
23
|
+
require_admin,
|
|
24
|
+
require_super_admin,
|
|
25
|
+
)
|
|
20
26
|
from lockbot.backend.app.auth.models import User
|
|
21
27
|
from lockbot.backend.app.auth.router import _generate_password, _hash_password
|
|
22
28
|
from lockbot.backend.app.auth.schemas import (
|
|
@@ -31,10 +37,6 @@ from lockbot.backend.app.database import get_db
|
|
|
31
37
|
|
|
32
38
|
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
|
33
39
|
|
|
34
|
-
ADMIN_VISIBLE_ROLES = ("admin", "user")
|
|
35
|
-
SUPER_ADMIN_VISIBLE_ROLES = ("super_admin", "admin", "user")
|
|
36
|
-
ADMIN_CREATABLE_ROLES = ("user",)
|
|
37
|
-
|
|
38
40
|
|
|
39
41
|
@router.post("/users", response_model=PasswordResetOut, status_code=201)
|
|
40
42
|
def admin_create_user(
|
|
@@ -43,12 +45,10 @@ def admin_create_user(
|
|
|
43
45
|
db: Session = Depends(get_db),
|
|
44
46
|
):
|
|
45
47
|
"""Admin creates a user. Super_admin can create admin/user; admin can only create user."""
|
|
46
|
-
#
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
if body.role not in SUPER_ADMIN_VISIBLE_ROLES:
|
|
51
|
-
raise HTTPException(status_code=400, detail=f"Invalid role, must be one of {SUPER_ADMIN_VISIBLE_ROLES}")
|
|
48
|
+
# Use unified permission check
|
|
49
|
+
allowed, status_code, error_msg = can_create_user_with_role(operator, body.role)
|
|
50
|
+
if not allowed:
|
|
51
|
+
raise HTTPException(status_code=status_code, detail=error_msg)
|
|
52
52
|
|
|
53
53
|
exists = db.query(User).filter(User.username == body.username).first()
|
|
54
54
|
if exists:
|
|
@@ -133,11 +133,13 @@ def admin_edit_user(
|
|
|
133
133
|
target.email = body.email
|
|
134
134
|
|
|
135
135
|
if body.role is not None:
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
136
|
+
# Use unified permission check
|
|
137
|
+
allowed, status_code, error_msg = can_assign_role(operator, target, body.role)
|
|
138
|
+
if not allowed:
|
|
139
|
+
raise HTTPException(status_code=status_code, detail=error_msg)
|
|
140
|
+
# Force logout when role changes
|
|
141
|
+
if target.role != body.role:
|
|
142
|
+
target.token_version = target.effective_token_version + 1
|
|
141
143
|
target.role = body.role
|
|
142
144
|
|
|
143
145
|
if body.max_running_bots is not None:
|
|
@@ -181,11 +183,30 @@ def admin_reset_password(
|
|
|
181
183
|
raw_password = _generate_password()
|
|
182
184
|
target.password_hash = _hash_password(raw_password)
|
|
183
185
|
target.must_change_password = True
|
|
186
|
+
# Force logout - invalidate all existing tokens
|
|
187
|
+
target.token_version = target.effective_token_version + 1
|
|
184
188
|
db.commit()
|
|
185
189
|
db.refresh(target)
|
|
186
190
|
return PasswordResetOut(id=target.id, username=target.username, new_password=raw_password)
|
|
187
191
|
|
|
188
192
|
|
|
193
|
+
@router.post("/users/{user_id}/force-logout")
|
|
194
|
+
def force_logout_user(
|
|
195
|
+
user_id: int,
|
|
196
|
+
operator: User = Depends(require_admin),
|
|
197
|
+
db: Session = Depends(get_db),
|
|
198
|
+
):
|
|
199
|
+
"""Force a user to logout by invalidating all their tokens."""
|
|
200
|
+
target = db.get(User, user_id)
|
|
201
|
+
if not target:
|
|
202
|
+
raise HTTPException(status_code=404, detail="User not found")
|
|
203
|
+
if not can_manage_user(operator, target.role):
|
|
204
|
+
raise HTTPException(status_code=403, detail="Cannot manage this user")
|
|
205
|
+
target.token_version = target.effective_token_version + 1
|
|
206
|
+
db.commit()
|
|
207
|
+
return {"id": target.id, "username": target.username, "message": "User logged out successfully"}
|
|
208
|
+
|
|
209
|
+
|
|
189
210
|
@router.get("/bots")
|
|
190
211
|
def list_all_bots(
|
|
191
212
|
_admin: User = Depends(require_admin),
|
|
@@ -23,10 +23,11 @@ _security = HTTPBearer(auto_error=False)
|
|
|
23
23
|
_ROLE_LEVELS = {"super_admin": 0, "admin": 1, "user": 2}
|
|
24
24
|
|
|
25
25
|
|
|
26
|
-
def create_access_token(user_id: int, must_change_password: bool = False) -> str:
|
|
26
|
+
def create_access_token(user_id: int, token_version: int = 0, must_change_password: bool = False) -> str:
|
|
27
27
|
payload = {
|
|
28
28
|
"sub": str(user_id),
|
|
29
29
|
"exp": datetime.now(timezone.utc) + timedelta(minutes=JWT_EXPIRE_MINUTES),
|
|
30
|
+
"ver": token_version,
|
|
30
31
|
"mcp": must_change_password,
|
|
31
32
|
}
|
|
32
33
|
return jwt.encode(payload, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
|
@@ -42,12 +43,18 @@ def get_current_user(
|
|
|
42
43
|
try:
|
|
43
44
|
payload = jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
|
44
45
|
user_id = int(payload["sub"])
|
|
46
|
+
token_version = payload.get("ver", 0) # Default to 0 for old tokens without version
|
|
45
47
|
except (jwt.PyJWTError, KeyError, ValueError):
|
|
46
48
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token") from None
|
|
47
49
|
|
|
48
50
|
user = db.get(User, user_id)
|
|
49
51
|
if user is None:
|
|
50
52
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
|
53
|
+
|
|
54
|
+
# Check token version - invalidate token if version mismatch
|
|
55
|
+
if user.effective_token_version != token_version:
|
|
56
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired, please login again")
|
|
57
|
+
|
|
51
58
|
return user
|
|
52
59
|
|
|
53
60
|
|
|
@@ -70,3 +77,44 @@ def can_manage_user(operator: User, target_role: str) -> bool:
|
|
|
70
77
|
op_level = _ROLE_LEVELS.get(operator.role, 3)
|
|
71
78
|
tgt_level = _ROLE_LEVELS.get(target_role, 3)
|
|
72
79
|
return op_level < tgt_level
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def can_assign_role(operator: User, target: User, new_role: str) -> tuple[bool, int, str]:
|
|
83
|
+
"""
|
|
84
|
+
Check if operator can change target's role to new_role.
|
|
85
|
+
|
|
86
|
+
Returns (allowed, http_status_code, error_message).
|
|
87
|
+
This consolidates all role assignment permission checks in one place.
|
|
88
|
+
"""
|
|
89
|
+
# Validate role
|
|
90
|
+
valid_roles = ("super_admin", "admin", "user")
|
|
91
|
+
if new_role not in valid_roles:
|
|
92
|
+
return False, 400, f"Invalid role, must be one of {valid_roles}"
|
|
93
|
+
|
|
94
|
+
# Cannot manage users at or above your own level
|
|
95
|
+
if not can_manage_user(operator, target.role):
|
|
96
|
+
return False, 403, "Cannot manage this user"
|
|
97
|
+
|
|
98
|
+
# Only super_admin can assign admin or super_admin role
|
|
99
|
+
if new_role in ("admin", "super_admin") and operator.role != "super_admin":
|
|
100
|
+
return False, 403, "Only super admin can assign this role"
|
|
101
|
+
|
|
102
|
+
return True, 200, ""
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def can_create_user_with_role(operator: User, role: str) -> tuple[bool, int, str]:
|
|
106
|
+
"""
|
|
107
|
+
Check if operator can create a user with the given role.
|
|
108
|
+
|
|
109
|
+
Returns (allowed, http_status_code, error_message).
|
|
110
|
+
"""
|
|
111
|
+
# Validate role
|
|
112
|
+
valid_roles = ("admin", "user")
|
|
113
|
+
if role not in valid_roles:
|
|
114
|
+
return False, 400, f"Invalid role, must be one of {valid_roles}"
|
|
115
|
+
|
|
116
|
+
# Only super_admin can create admin
|
|
117
|
+
if role == "admin" and operator.role != "super_admin":
|
|
118
|
+
return False, 403, "Only super admin can create user with this role"
|
|
119
|
+
|
|
120
|
+
return True, 200, ""
|
|
@@ -20,5 +20,11 @@ class User(Base):
|
|
|
20
20
|
role: Mapped[str] = mapped_column(String(16), nullable=False, default="user")
|
|
21
21
|
max_running_bots: Mapped[int] = mapped_column(Integer, default=10)
|
|
22
22
|
must_change_password: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False)
|
|
23
|
+
token_version: Mapped[int] = mapped_column(Integer, nullable=False, default=0)
|
|
23
24
|
created_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now())
|
|
24
25
|
updated_at: Mapped[datetime] = mapped_column(DateTime, server_default=func.now(), onupdate=func.now())
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def effective_token_version(self) -> int:
|
|
29
|
+
"""Return token_version, treating NULL (from pre-migration rows) as 0."""
|
|
30
|
+
return self.token_version if self.token_version is not None else 0
|
|
@@ -75,7 +75,7 @@ def login(body: UserLogin, db: Session = Depends(get_db)):
|
|
|
75
75
|
if not user or not _verify_password(body.password, user.password_hash):
|
|
76
76
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
|
77
77
|
return TokenOut(
|
|
78
|
-
access_token=create_access_token(user.id, user.must_change_password),
|
|
78
|
+
access_token=create_access_token(user.id, user.effective_token_version, user.must_change_password),
|
|
79
79
|
must_change_password=user.must_change_password,
|
|
80
80
|
)
|
|
81
81
|
|
|
@@ -99,7 +99,7 @@ def change_password(
|
|
|
99
99
|
user.must_change_password = False
|
|
100
100
|
db.commit()
|
|
101
101
|
return TokenOut(
|
|
102
|
-
access_token=create_access_token(user.id, False),
|
|
102
|
+
access_token=create_access_token(user.id, user.effective_token_version, False),
|
|
103
103
|
must_change_password=False,
|
|
104
104
|
)
|
|
105
105
|
|
|
@@ -120,7 +120,7 @@ def force_change_password(
|
|
|
120
120
|
user.must_change_password = False
|
|
121
121
|
db.commit()
|
|
122
122
|
return TokenOut(
|
|
123
|
-
access_token=create_access_token(user.id, False),
|
|
123
|
+
access_token=create_access_token(user.id, user.effective_token_version, False),
|
|
124
124
|
must_change_password=False,
|
|
125
125
|
)
|
|
126
126
|
|
|
@@ -400,6 +400,10 @@ def start_bot(
|
|
|
400
400
|
):
|
|
401
401
|
bot = _get_user_bot(bot_id, user, db)
|
|
402
402
|
|
|
403
|
+
# Only owner or super_admin can start
|
|
404
|
+
if user.role != "super_admin" and bot.user_id != user.id:
|
|
405
|
+
raise HTTPException(status_code=403, detail="Cannot start another user's bot")
|
|
406
|
+
|
|
403
407
|
if bot.status == "running":
|
|
404
408
|
return BotStatusOut(
|
|
405
409
|
id=bot.id,
|
|
@@ -462,6 +466,10 @@ def stop_bot(
|
|
|
462
466
|
):
|
|
463
467
|
bot = _get_user_bot(bot_id, user, db)
|
|
464
468
|
|
|
469
|
+
# Only owner or super_admin can stop
|
|
470
|
+
if user.role != "super_admin" and bot.user_id != user.id:
|
|
471
|
+
raise HTTPException(status_code=403, detail="Cannot stop another user's bot")
|
|
472
|
+
|
|
465
473
|
if bot.status not in ("running", "error"):
|
|
466
474
|
raise HTTPException(status_code=409, detail=f"Bot is not running (status={bot.status})")
|
|
467
475
|
|
|
@@ -485,6 +493,10 @@ def restart_bot(
|
|
|
485
493
|
):
|
|
486
494
|
bot = _get_user_bot(bot_id, user, db)
|
|
487
495
|
|
|
496
|
+
# Only owner or super_admin can restart
|
|
497
|
+
if user.role != "super_admin" and bot.user_id != user.id:
|
|
498
|
+
raise HTTPException(status_code=403, detail="Cannot restart another user's bot")
|
|
499
|
+
|
|
488
500
|
config_dict = _build_config_dict(bot, db)
|
|
489
501
|
|
|
490
502
|
try:
|
|
@@ -785,11 +797,9 @@ def update_bot_state(
|
|
|
785
797
|
|
|
786
798
|
bot = _get_user_bot(bot_id, user, db)
|
|
787
799
|
|
|
788
|
-
# Admin
|
|
789
|
-
if user.role
|
|
790
|
-
|
|
791
|
-
if owner and owner.role in ("admin", "super_admin"):
|
|
792
|
-
raise HTTPException(status_code=403, detail="Cannot edit another admin's bot state")
|
|
800
|
+
# Admin can only edit own bot state, super_admin can edit any
|
|
801
|
+
if user.role != "super_admin" and bot.user_id != user.id:
|
|
802
|
+
raise HTTPException(status_code=403, detail="Cannot edit another user's bot state")
|
|
793
803
|
|
|
794
804
|
if bot.status == "running":
|
|
795
805
|
raise HTTPException(status_code=409, detail="Stop the bot before editing state")
|
|
@@ -91,6 +91,21 @@ def _migrate_bot_soft_delete():
|
|
|
91
91
|
logger.info("Migrated bots: added 'deleted_at' column")
|
|
92
92
|
|
|
93
93
|
|
|
94
|
+
def _migrate_users_token_version():
|
|
95
|
+
"""Add 'token_version' column to users if it doesn't exist (SQLite migration)."""
|
|
96
|
+
from sqlalchemy import inspect as sa_inspect
|
|
97
|
+
from sqlalchemy import text
|
|
98
|
+
|
|
99
|
+
insp = sa_inspect(engine)
|
|
100
|
+
if "users" not in insp.get_table_names():
|
|
101
|
+
return
|
|
102
|
+
columns = [c["name"] for c in insp.get_columns("users")]
|
|
103
|
+
if "token_version" not in columns:
|
|
104
|
+
with engine.begin() as conn:
|
|
105
|
+
conn.execute(text("ALTER TABLE users ADD COLUMN token_version INTEGER NOT NULL DEFAULT 0"))
|
|
106
|
+
logger.info("Migrated users: added 'token_version' column")
|
|
107
|
+
|
|
108
|
+
|
|
94
109
|
def _seed_dev_admin():
|
|
95
110
|
"""Create admin user in dev mode if it doesn't exist."""
|
|
96
111
|
from lockbot.backend.app.config import (
|
|
@@ -172,6 +187,7 @@ async def lifespan(app: FastAPI):
|
|
|
172
187
|
_migrate_bot_logs_category()
|
|
173
188
|
_migrate_bot_consecutive_failures()
|
|
174
189
|
_migrate_users_must_change_password()
|
|
190
|
+
_migrate_users_token_version()
|
|
175
191
|
_migrate_bot_soft_delete()
|
|
176
192
|
_seed_dev_admin()
|
|
177
193
|
_seed_dev_users()
|
|
@@ -7,10 +7,12 @@ from datetime import datetime
|
|
|
7
7
|
|
|
8
8
|
from fastapi import APIRouter, Depends, HTTPException
|
|
9
9
|
from pydantic import BaseModel
|
|
10
|
+
from sqlalchemy import func as sa_func
|
|
10
11
|
from sqlalchemy.orm import Session
|
|
11
12
|
|
|
12
|
-
from lockbot.backend.app.auth.dependencies import require_super_admin
|
|
13
|
+
from lockbot.backend.app.auth.dependencies import get_current_user, require_super_admin
|
|
13
14
|
from lockbot.backend.app.auth.models import User
|
|
15
|
+
from lockbot.backend.app.bots.models import Bot
|
|
14
16
|
from lockbot.backend.app.database import get_db
|
|
15
17
|
from lockbot.backend.app.settings.models import SiteSetting
|
|
16
18
|
|
|
@@ -64,6 +66,30 @@ def public_settings(db: Session = Depends(get_db)):
|
|
|
64
66
|
return [SettingOut(key=k, value=_get_setting(db, k)) for k in sorted(PUBLIC_KEYS)]
|
|
65
67
|
|
|
66
68
|
|
|
69
|
+
class PlatformStats(BaseModel):
|
|
70
|
+
"""Public platform statistics."""
|
|
71
|
+
|
|
72
|
+
total_users: int
|
|
73
|
+
total_bots: int
|
|
74
|
+
running_bots: int
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@router.get("/api/public/stats", response_model=PlatformStats)
|
|
78
|
+
def get_public_stats(
|
|
79
|
+
_user: User = Depends(get_current_user),
|
|
80
|
+
db: Session = Depends(get_db),
|
|
81
|
+
):
|
|
82
|
+
"""Return platform statistics (requires login)."""
|
|
83
|
+
total_users = db.query(sa_func.count(User.id)).scalar()
|
|
84
|
+
total_bots = db.query(sa_func.count(Bot.id)).filter(Bot.is_deleted.is_(False)).scalar()
|
|
85
|
+
running_bots = db.query(sa_func.count(Bot.id)).filter(Bot.is_deleted.is_(False), Bot.status == "running").scalar()
|
|
86
|
+
return PlatformStats(
|
|
87
|
+
total_users=total_users,
|
|
88
|
+
total_bots=total_bots,
|
|
89
|
+
running_bots=running_bots,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
67
93
|
# ── Admin endpoints (super_admin only) ──────────────────
|
|
68
94
|
|
|
69
95
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|