remoteRF-server-testing 0.0.0__py3-none-any.whl
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.
- remoteRF_server/__init__.py +0 -0
- remoteRF_server/common/__init__.py +0 -0
- remoteRF_server/common/grpc/__init__.py +1 -0
- remoteRF_server/common/grpc/grpc_host_pb2.py +63 -0
- remoteRF_server/common/grpc/grpc_host_pb2_grpc.py +97 -0
- remoteRF_server/common/grpc/grpc_pb2.py +59 -0
- remoteRF_server/common/grpc/grpc_pb2_grpc.py +97 -0
- remoteRF_server/common/idl/__init__.py +1 -0
- remoteRF_server/common/idl/device_schema.py +39 -0
- remoteRF_server/common/idl/pluto_schema.py +174 -0
- remoteRF_server/common/idl/schema.py +358 -0
- remoteRF_server/common/utils/__init__.py +6 -0
- remoteRF_server/common/utils/ansi_codes.py +120 -0
- remoteRF_server/common/utils/api_token.py +21 -0
- remoteRF_server/common/utils/db_connection.py +35 -0
- remoteRF_server/common/utils/db_location.py +24 -0
- remoteRF_server/common/utils/list_string.py +5 -0
- remoteRF_server/common/utils/process_arg.py +80 -0
- remoteRF_server/drivers/__init__.py +0 -0
- remoteRF_server/drivers/adalm_pluto/__init__.py +0 -0
- remoteRF_server/drivers/adalm_pluto/pluto_remote_server.py +105 -0
- remoteRF_server/host/__init__.py +0 -0
- remoteRF_server/host/host_auth_token.py +292 -0
- remoteRF_server/host/host_directory_store.py +142 -0
- remoteRF_server/host/host_tunnel_server.py +1388 -0
- remoteRF_server/server/__init__.py +0 -0
- remoteRF_server/server/acc_perms.py +317 -0
- remoteRF_server/server/cert_provider.py +184 -0
- remoteRF_server/server/device_manager.py +688 -0
- remoteRF_server/server/grpc_server.py +1023 -0
- remoteRF_server/server/reservation.py +811 -0
- remoteRF_server/server/rpc_manager.py +104 -0
- remoteRF_server/server/user_group_cli.py +723 -0
- remoteRF_server/server/user_group_handler.py +1120 -0
- remoteRF_server/serverrf_cli.py +1377 -0
- remoteRF_server/tools/__init__.py +191 -0
- remoteRF_server/tools/gen_certs.py +274 -0
- remoteRF_server/tools/gist_status.py +139 -0
- remoteRF_server/tools/gist_status_testing.py +67 -0
- remoterf_server_testing-0.0.0.dist-info/METADATA +612 -0
- remoterf_server_testing-0.0.0.dist-info/RECORD +44 -0
- remoterf_server_testing-0.0.0.dist-info/WHEEL +5 -0
- remoterf_server_testing-0.0.0.dist-info/entry_points.txt +2 -0
- remoterf_server_testing-0.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,1120 @@
|
|
|
1
|
+
import sqlite3
|
|
2
|
+
from datetime import datetime
|
|
3
|
+
import time
|
|
4
|
+
import sys
|
|
5
|
+
import json
|
|
6
|
+
import threading
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from functools import wraps
|
|
9
|
+
from typing import Optional, Tuple, List
|
|
10
|
+
|
|
11
|
+
from ..common.utils import *
|
|
12
|
+
|
|
13
|
+
import os
|
|
14
|
+
import sys
|
|
15
|
+
|
|
16
|
+
class UserGroupHandler:
|
|
17
|
+
'''
|
|
18
|
+
Handles user groups and enrollment codes.
|
|
19
|
+
create_user_group -> bool
|
|
20
|
+
delete_user_group -> bool
|
|
21
|
+
edit_user_group -> bool
|
|
22
|
+
get_all_user_groups -> list[dict]
|
|
23
|
+
create_enrollment_codes -> str
|
|
24
|
+
generate_random_enrollment_code_names -> list[str]
|
|
25
|
+
delete_enrollment_code (by code_name) -> bool
|
|
26
|
+
delete_enrollment_code (by group_id) -> bool
|
|
27
|
+
get_all_enrollment_codes -> list[dict]
|
|
28
|
+
enroll_user_with_code -> bool
|
|
29
|
+
get_users_devices -> list[int]
|
|
30
|
+
get_users_max_reservation_time -> tuple[bool, int]
|
|
31
|
+
get_users_max_reservations -> tuple[bool, int]
|
|
32
|
+
'''
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
self.filepath = get_db_dir() / 'usergroups.db'
|
|
36
|
+
|
|
37
|
+
# print("DB path:", self.filepath)
|
|
38
|
+
# print("Parent exists:", self.filepath.parent.exists(), "is_dir:", self.filepath.parent.is_dir())
|
|
39
|
+
# print("Writable:", os.access(self.filepath.parent, os.W_OK))
|
|
40
|
+
|
|
41
|
+
self.db = sqlite3.connect(self.filepath)
|
|
42
|
+
self.cursor = self.db.cursor()
|
|
43
|
+
|
|
44
|
+
self.cursor.execute('''
|
|
45
|
+
CREATE TABLE IF NOT EXISTS UserGroups (
|
|
46
|
+
group_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
47
|
+
group_name TEXT NOT NULL UNIQUE,
|
|
48
|
+
devices_whitelist TEXT NOT NULL DEFAULT '[]', -- JSON array, e.g. "[0,1,2]"
|
|
49
|
+
max_reservation_time_sec INTEGER NOT NULL,
|
|
50
|
+
max_reservations INTEGER NOT NULL,
|
|
51
|
+
lifetime_sec INTEGER NOT NULL,
|
|
52
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
53
|
+
);
|
|
54
|
+
''')
|
|
55
|
+
|
|
56
|
+
self.cursor.execute('''
|
|
57
|
+
CREATE TABLE IF NOT EXISTS UsersLookup (
|
|
58
|
+
user_id INTEGER NOT NULL,
|
|
59
|
+
group_id INTEGER NOT NULL,
|
|
60
|
+
PRIMARY KEY (user_id, group_id)
|
|
61
|
+
);
|
|
62
|
+
''')
|
|
63
|
+
|
|
64
|
+
self.cursor.execute('''
|
|
65
|
+
CREATE TABLE IF NOT EXISTS EnrollmentCodes (
|
|
66
|
+
code_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
67
|
+
code_name TEXT NOT NULL UNIQUE,
|
|
68
|
+
group_id INTEGER NOT NULL,
|
|
69
|
+
uses INTEGER NOT NULL,
|
|
70
|
+
duration_sec INTEGER NOT NULL,
|
|
71
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
72
|
+
);
|
|
73
|
+
''')
|
|
74
|
+
|
|
75
|
+
self.cursor.close()
|
|
76
|
+
self.db.commit()
|
|
77
|
+
self.db.close()
|
|
78
|
+
self.lock = threading.RLock()
|
|
79
|
+
|
|
80
|
+
@db_connection
|
|
81
|
+
def create_user_group(
|
|
82
|
+
self,
|
|
83
|
+
*,
|
|
84
|
+
group_name: str,
|
|
85
|
+
devices_whitelist: list[int],
|
|
86
|
+
max_reservation_time_sec: int,
|
|
87
|
+
max_reservations: int,
|
|
88
|
+
lifetime_sec: int,
|
|
89
|
+
) -> bool:
|
|
90
|
+
try:
|
|
91
|
+
if not isinstance(group_name, str) or not group_name.strip():
|
|
92
|
+
return False
|
|
93
|
+
|
|
94
|
+
if devices_whitelist is None:
|
|
95
|
+
devices_whitelist = []
|
|
96
|
+
if not isinstance(devices_whitelist, (list, tuple)):
|
|
97
|
+
return False
|
|
98
|
+
|
|
99
|
+
try:
|
|
100
|
+
devices_whitelist = sorted({int(d) for d in devices_whitelist})
|
|
101
|
+
except Exception:
|
|
102
|
+
return False
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
max_reservation_time_sec = int(max_reservation_time_sec)
|
|
106
|
+
max_reservations = int(max_reservations)
|
|
107
|
+
lifetime_sec = int(lifetime_sec)
|
|
108
|
+
except Exception:
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
if max_reservation_time_sec <= 0:
|
|
112
|
+
return False
|
|
113
|
+
if max_reservations < 0:
|
|
114
|
+
return False
|
|
115
|
+
if lifetime_sec < 0:
|
|
116
|
+
return False
|
|
117
|
+
|
|
118
|
+
devices_json = json.dumps(devices_whitelist)
|
|
119
|
+
|
|
120
|
+
# ---- insert ----
|
|
121
|
+
self.cursor.execute(
|
|
122
|
+
"""
|
|
123
|
+
INSERT INTO UserGroups
|
|
124
|
+
(group_name, devices_whitelist, max_reservation_time_sec, max_reservations, lifetime_sec, created_at)
|
|
125
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
126
|
+
""",
|
|
127
|
+
(
|
|
128
|
+
group_name.strip(),
|
|
129
|
+
devices_json,
|
|
130
|
+
max_reservation_time_sec,
|
|
131
|
+
max_reservations,
|
|
132
|
+
lifetime_sec,
|
|
133
|
+
datetime.now(),
|
|
134
|
+
),
|
|
135
|
+
)
|
|
136
|
+
return True
|
|
137
|
+
|
|
138
|
+
except Exception:
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
@db_connection
|
|
142
|
+
def get_groups_user_in(self, *, uid: int) -> list[str]:
|
|
143
|
+
try:
|
|
144
|
+
uid = int(uid)
|
|
145
|
+
if uid < 0:
|
|
146
|
+
return []
|
|
147
|
+
|
|
148
|
+
# 1) Fetch group_ids the user is in
|
|
149
|
+
self.cursor.execute(
|
|
150
|
+
"SELECT group_id FROM UsersLookup WHERE user_id = ?",
|
|
151
|
+
(uid,),
|
|
152
|
+
)
|
|
153
|
+
gid_rows = self.cursor.fetchall() or []
|
|
154
|
+
group_ids = []
|
|
155
|
+
for r in gid_rows:
|
|
156
|
+
if not r or r[0] is None:
|
|
157
|
+
continue
|
|
158
|
+
try:
|
|
159
|
+
gid = int(r[0])
|
|
160
|
+
except Exception:
|
|
161
|
+
continue
|
|
162
|
+
if gid > 0:
|
|
163
|
+
group_ids.append(gid)
|
|
164
|
+
|
|
165
|
+
if not group_ids:
|
|
166
|
+
return []
|
|
167
|
+
|
|
168
|
+
# de-dupe, stable order
|
|
169
|
+
seen_gid = set()
|
|
170
|
+
group_ids_unique = []
|
|
171
|
+
for gid in group_ids:
|
|
172
|
+
if gid in seen_gid:
|
|
173
|
+
continue
|
|
174
|
+
seen_gid.add(gid)
|
|
175
|
+
group_ids_unique.append(gid)
|
|
176
|
+
|
|
177
|
+
# 2) Fetch group names from UserGroups using those ids
|
|
178
|
+
placeholders = ",".join(["?"] * len(group_ids_unique))
|
|
179
|
+
self.cursor.execute(
|
|
180
|
+
f"""
|
|
181
|
+
SELECT group_name
|
|
182
|
+
FROM UserGroups
|
|
183
|
+
WHERE group_id IN ({placeholders})
|
|
184
|
+
ORDER BY group_name ASC
|
|
185
|
+
""",
|
|
186
|
+
tuple(group_ids_unique),
|
|
187
|
+
)
|
|
188
|
+
rows = self.cursor.fetchall() or []
|
|
189
|
+
|
|
190
|
+
# de-dupe defensively, preserve order
|
|
191
|
+
out: list[str] = []
|
|
192
|
+
seen_name: set[str] = set()
|
|
193
|
+
for r in rows:
|
|
194
|
+
if not r or r[0] is None:
|
|
195
|
+
continue
|
|
196
|
+
name = str(r[0]).strip()
|
|
197
|
+
if not name or name in seen_name:
|
|
198
|
+
continue
|
|
199
|
+
seen_name.add(name)
|
|
200
|
+
out.append(name)
|
|
201
|
+
|
|
202
|
+
return out
|
|
203
|
+
except Exception:
|
|
204
|
+
return []
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@db_connection
|
|
209
|
+
def delete_user_group(
|
|
210
|
+
self,
|
|
211
|
+
*,
|
|
212
|
+
group_id: int
|
|
213
|
+
) -> bool:
|
|
214
|
+
try:
|
|
215
|
+
try:
|
|
216
|
+
group_id = int(group_id)
|
|
217
|
+
except Exception:
|
|
218
|
+
return False
|
|
219
|
+
if group_id <= 0:
|
|
220
|
+
return False
|
|
221
|
+
|
|
222
|
+
self.cursor.execute("SELECT 1 FROM UserGroups WHERE group_id = ? LIMIT 1", (group_id,))
|
|
223
|
+
if self.cursor.fetchone() is None:
|
|
224
|
+
return False
|
|
225
|
+
|
|
226
|
+
# cleanup
|
|
227
|
+
self.cursor.execute("DELETE FROM EnrollmentCodes WHERE group_id = ?", (group_id,))
|
|
228
|
+
self.cursor.execute("DELETE FROM UsersLookup WHERE group_id = ?", (group_id,))
|
|
229
|
+
self.cursor.execute("DELETE FROM UserGroups WHERE group_id = ?", (group_id,))
|
|
230
|
+
|
|
231
|
+
return True
|
|
232
|
+
except Exception:
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
@db_connection
|
|
236
|
+
def edit_user_group(
|
|
237
|
+
self,
|
|
238
|
+
*,
|
|
239
|
+
group_id: int,
|
|
240
|
+
group_name: str = None,
|
|
241
|
+
devices_whitelist: list[int] = None,
|
|
242
|
+
max_reservation_time_sec: int = None,
|
|
243
|
+
max_reservations: int = None,
|
|
244
|
+
lifetime_sec: int = None
|
|
245
|
+
) -> bool:
|
|
246
|
+
try:
|
|
247
|
+
try:
|
|
248
|
+
group_id = int(group_id)
|
|
249
|
+
except Exception:
|
|
250
|
+
return False
|
|
251
|
+
if group_id <= 0:
|
|
252
|
+
return False
|
|
253
|
+
|
|
254
|
+
self.cursor.execute("SELECT 1 FROM UserGroups WHERE group_id = ? LIMIT 1", (group_id,))
|
|
255
|
+
if self.cursor.fetchone() is None:
|
|
256
|
+
return False
|
|
257
|
+
|
|
258
|
+
# dynamic update
|
|
259
|
+
sets = []
|
|
260
|
+
params = []
|
|
261
|
+
|
|
262
|
+
if group_name is not None:
|
|
263
|
+
group_name = str(group_name).strip()
|
|
264
|
+
if group_name == "":
|
|
265
|
+
return False
|
|
266
|
+
sets.append("group_name = ?")
|
|
267
|
+
params.append(group_name)
|
|
268
|
+
|
|
269
|
+
if devices_whitelist is not None:
|
|
270
|
+
try:
|
|
271
|
+
wl = [int(x) for x in list(devices_whitelist)]
|
|
272
|
+
except Exception:
|
|
273
|
+
return False
|
|
274
|
+
wl = sorted(set(wl))
|
|
275
|
+
sets.append("devices_whitelist = ?")
|
|
276
|
+
params.append(json.dumps(wl))
|
|
277
|
+
|
|
278
|
+
if max_reservation_time_sec is not None:
|
|
279
|
+
try:
|
|
280
|
+
v = int(max_reservation_time_sec)
|
|
281
|
+
except Exception:
|
|
282
|
+
return False
|
|
283
|
+
if v <= 0:
|
|
284
|
+
return False
|
|
285
|
+
sets.append("max_reservation_time_sec = ?")
|
|
286
|
+
params.append(v)
|
|
287
|
+
|
|
288
|
+
if max_reservations is not None:
|
|
289
|
+
try:
|
|
290
|
+
v = int(max_reservations)
|
|
291
|
+
except Exception:
|
|
292
|
+
return False
|
|
293
|
+
if v <= 0:
|
|
294
|
+
return False
|
|
295
|
+
sets.append("max_reservations = ?")
|
|
296
|
+
params.append(v)
|
|
297
|
+
|
|
298
|
+
if lifetime_sec is not None:
|
|
299
|
+
try:
|
|
300
|
+
v = int(lifetime_sec)
|
|
301
|
+
except Exception:
|
|
302
|
+
return False
|
|
303
|
+
if v <= 0:
|
|
304
|
+
return False
|
|
305
|
+
sets.append("lifetime_sec = ?")
|
|
306
|
+
params.append(v)
|
|
307
|
+
|
|
308
|
+
if not sets:
|
|
309
|
+
return False
|
|
310
|
+
|
|
311
|
+
params.append(group_id)
|
|
312
|
+
sql = f"UPDATE UserGroups SET {', '.join(sets)} WHERE group_id = ?"
|
|
313
|
+
self.cursor.execute(sql, tuple(params))
|
|
314
|
+
return True
|
|
315
|
+
except Exception:
|
|
316
|
+
return False
|
|
317
|
+
|
|
318
|
+
@db_connection
|
|
319
|
+
def get_all_user_groups(self, include_users: bool) -> list[dict]:
|
|
320
|
+
try:
|
|
321
|
+
include_users = bool(include_users)
|
|
322
|
+
|
|
323
|
+
self.cursor.execute("""
|
|
324
|
+
SELECT
|
|
325
|
+
group_id,
|
|
326
|
+
group_name,
|
|
327
|
+
devices_whitelist,
|
|
328
|
+
max_reservation_time_sec,
|
|
329
|
+
max_reservations,
|
|
330
|
+
lifetime_sec,
|
|
331
|
+
created_at
|
|
332
|
+
FROM UserGroups
|
|
333
|
+
ORDER BY group_id ASC
|
|
334
|
+
""")
|
|
335
|
+
rows = self.cursor.fetchall() or []
|
|
336
|
+
|
|
337
|
+
groups: list[dict] = []
|
|
338
|
+
gid_to_users: dict[int, list[int]] = {}
|
|
339
|
+
|
|
340
|
+
if include_users:
|
|
341
|
+
self.cursor.execute("""
|
|
342
|
+
SELECT user_id, group_id
|
|
343
|
+
FROM UsersLookup
|
|
344
|
+
""")
|
|
345
|
+
urows = self.cursor.fetchall() or []
|
|
346
|
+
for user_id, group_id in urows:
|
|
347
|
+
try:
|
|
348
|
+
gid = int(group_id)
|
|
349
|
+
uid = int(user_id)
|
|
350
|
+
except Exception:
|
|
351
|
+
continue
|
|
352
|
+
gid_to_users.setdefault(gid, []).append(uid)
|
|
353
|
+
|
|
354
|
+
# sort
|
|
355
|
+
for gid in gid_to_users:
|
|
356
|
+
gid_to_users[gid] = sorted(set(gid_to_users[gid]))
|
|
357
|
+
|
|
358
|
+
for r in rows:
|
|
359
|
+
try:
|
|
360
|
+
gid = int(r[0])
|
|
361
|
+
name = str(r[1])
|
|
362
|
+
wl_raw = r[2] if r[2] is not None else "[]"
|
|
363
|
+
try:
|
|
364
|
+
wl = json.loads(wl_raw) if isinstance(wl_raw, str) else wl_raw
|
|
365
|
+
wl = [int(x) for x in (wl or [])]
|
|
366
|
+
wl = sorted(set(wl))
|
|
367
|
+
except Exception:
|
|
368
|
+
wl = []
|
|
369
|
+
max_t = int(r[3])
|
|
370
|
+
max_r = int(r[4])
|
|
371
|
+
life = int(r[5])
|
|
372
|
+
created_at = r[6]
|
|
373
|
+
except Exception:
|
|
374
|
+
continue
|
|
375
|
+
|
|
376
|
+
entry = {
|
|
377
|
+
"group_id": gid,
|
|
378
|
+
"group_name": name,
|
|
379
|
+
"devices_whitelist": wl,
|
|
380
|
+
"max_reservation_time_sec": max_t,
|
|
381
|
+
"max_reservations": max_r,
|
|
382
|
+
"lifetime_sec": life,
|
|
383
|
+
"created_at": str(created_at) if created_at is not None else None,
|
|
384
|
+
}
|
|
385
|
+
if include_users:
|
|
386
|
+
entry["users"] = gid_to_users.get(gid, [])
|
|
387
|
+
groups.append(entry)
|
|
388
|
+
|
|
389
|
+
return groups
|
|
390
|
+
except Exception:
|
|
391
|
+
return []
|
|
392
|
+
|
|
393
|
+
@db_connection
|
|
394
|
+
def create_enrollment_codes(
|
|
395
|
+
self,
|
|
396
|
+
*,
|
|
397
|
+
code_names: list[str],
|
|
398
|
+
group_id: int,
|
|
399
|
+
uses: int,
|
|
400
|
+
duration_sec: int
|
|
401
|
+
) -> str:
|
|
402
|
+
try:
|
|
403
|
+
# Basic sanitize / type normalization
|
|
404
|
+
if not isinstance(code_names, list) or len(code_names) == 0:
|
|
405
|
+
return ""
|
|
406
|
+
|
|
407
|
+
group_id = int(group_id)
|
|
408
|
+
uses = int(uses)
|
|
409
|
+
duration_sec = int(duration_sec)
|
|
410
|
+
|
|
411
|
+
if group_id < 0 or uses <= 0 or duration_sec < 0:
|
|
412
|
+
return ""
|
|
413
|
+
|
|
414
|
+
# Ensure group exists
|
|
415
|
+
self.cursor.execute("SELECT 1 FROM UserGroups WHERE group_id = ? LIMIT 1", (group_id,))
|
|
416
|
+
if self.cursor.fetchone() is None:
|
|
417
|
+
return ""
|
|
418
|
+
|
|
419
|
+
# Clean + de-dupe (preserve order)
|
|
420
|
+
cleaned: list[str] = []
|
|
421
|
+
seen: set[str] = set()
|
|
422
|
+
for c in code_names:
|
|
423
|
+
if not isinstance(c, str):
|
|
424
|
+
continue
|
|
425
|
+
c = c.strip()
|
|
426
|
+
if not c:
|
|
427
|
+
continue
|
|
428
|
+
if c in seen:
|
|
429
|
+
continue
|
|
430
|
+
seen.add(c)
|
|
431
|
+
cleaned.append(c)
|
|
432
|
+
|
|
433
|
+
if not cleaned:
|
|
434
|
+
return ""
|
|
435
|
+
|
|
436
|
+
# Insert many, silently skip conflicts/invalid rows
|
|
437
|
+
created = 0
|
|
438
|
+
for code in cleaned:
|
|
439
|
+
try:
|
|
440
|
+
self.cursor.execute(
|
|
441
|
+
"""
|
|
442
|
+
INSERT OR IGNORE INTO EnrollmentCodes
|
|
443
|
+
(code_name, group_id, uses, duration_sec, created_at)
|
|
444
|
+
VALUES
|
|
445
|
+
(?, ?, ?, ?, ?)
|
|
446
|
+
""",
|
|
447
|
+
(code, group_id, uses, duration_sec, datetime.now()),
|
|
448
|
+
)
|
|
449
|
+
if getattr(self.cursor, "rowcount", 0) == 1:
|
|
450
|
+
created += 1
|
|
451
|
+
except Exception:
|
|
452
|
+
continue
|
|
453
|
+
|
|
454
|
+
return f"Created {created}/{len(cleaned)} enrollment codes."
|
|
455
|
+
except Exception:
|
|
456
|
+
return ""
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
@db_connection
|
|
460
|
+
def generate_random_enrollment_code_names(
|
|
461
|
+
self, *, count: int, char_count: int
|
|
462
|
+
) -> list[str]:
|
|
463
|
+
try:
|
|
464
|
+
count = int(count)
|
|
465
|
+
char_count = int(char_count)
|
|
466
|
+
|
|
467
|
+
if count <= 0 or char_count <= 0:
|
|
468
|
+
return []
|
|
469
|
+
|
|
470
|
+
import secrets
|
|
471
|
+
import string
|
|
472
|
+
|
|
473
|
+
alphabet = string.ascii_uppercase + string.digits
|
|
474
|
+
|
|
475
|
+
# Pull existing codes once so we can reroll locally without hammering the DB.
|
|
476
|
+
try:
|
|
477
|
+
self.cursor.execute("SELECT code_name FROM EnrollmentCodes")
|
|
478
|
+
existing_rows = self.cursor.fetchall() or []
|
|
479
|
+
existing: set[str] = {
|
|
480
|
+
str(r[0]).strip() for r in existing_rows if r and r[0] is not None and str(r[0]).strip()
|
|
481
|
+
}
|
|
482
|
+
except Exception:
|
|
483
|
+
existing = set()
|
|
484
|
+
|
|
485
|
+
created: list[str] = []
|
|
486
|
+
used: set[str] = set(existing) # prevents collisions with DB + within this batch
|
|
487
|
+
|
|
488
|
+
max_attempts = max(1000, count * 200)
|
|
489
|
+
attempts = 0
|
|
490
|
+
|
|
491
|
+
while len(created) < count and attempts < max_attempts:
|
|
492
|
+
attempts += 1
|
|
493
|
+
code = "".join(secrets.choice(alphabet) for _ in range(char_count))
|
|
494
|
+
if code in used:
|
|
495
|
+
continue
|
|
496
|
+
used.add(code)
|
|
497
|
+
created.append(code)
|
|
498
|
+
|
|
499
|
+
return created
|
|
500
|
+
except Exception:
|
|
501
|
+
return []
|
|
502
|
+
|
|
503
|
+
@db_connection
|
|
504
|
+
def delete_enrollment_code_name(self, *, code_name: str) -> bool:
|
|
505
|
+
try:
|
|
506
|
+
if not isinstance(code_name, str):
|
|
507
|
+
return False
|
|
508
|
+
|
|
509
|
+
code_name = code_name.strip()
|
|
510
|
+
if not code_name:
|
|
511
|
+
return False
|
|
512
|
+
|
|
513
|
+
self.cursor.execute(
|
|
514
|
+
"SELECT 1 FROM EnrollmentCodes WHERE code_name = ? LIMIT 1",
|
|
515
|
+
(code_name,),
|
|
516
|
+
)
|
|
517
|
+
if self.cursor.fetchone() is None:
|
|
518
|
+
return False
|
|
519
|
+
|
|
520
|
+
self.cursor.execute(
|
|
521
|
+
"DELETE FROM EnrollmentCodes WHERE code_name = ?",
|
|
522
|
+
(code_name,),
|
|
523
|
+
)
|
|
524
|
+
return self.cursor.rowcount > 0
|
|
525
|
+
except Exception:
|
|
526
|
+
return False
|
|
527
|
+
|
|
528
|
+
@db_connection
|
|
529
|
+
def delete_enrollment_code_id(self, *, group_id: int) -> bool:
|
|
530
|
+
try:
|
|
531
|
+
group_id = int(group_id)
|
|
532
|
+
if group_id < 0:
|
|
533
|
+
return False
|
|
534
|
+
|
|
535
|
+
self.cursor.execute(
|
|
536
|
+
"DELETE FROM EnrollmentCodes WHERE group_id = ?",
|
|
537
|
+
(group_id,),
|
|
538
|
+
)
|
|
539
|
+
return self.cursor.rowcount > 0
|
|
540
|
+
except Exception:
|
|
541
|
+
return False
|
|
542
|
+
|
|
543
|
+
@db_connection
|
|
544
|
+
def get_all_enrollment_codes(self) -> list[dict]:
|
|
545
|
+
try:
|
|
546
|
+
self.cursor.execute(
|
|
547
|
+
"SELECT code_id, code_name, group_id, uses, duration_sec FROM EnrollmentCodes"
|
|
548
|
+
)
|
|
549
|
+
rows = self.cursor.fetchall() or []
|
|
550
|
+
|
|
551
|
+
out: list[dict] = []
|
|
552
|
+
for r in rows:
|
|
553
|
+
try:
|
|
554
|
+
out.append(
|
|
555
|
+
{
|
|
556
|
+
"code_id": int(r[0]),
|
|
557
|
+
"code_name": str(r[1]),
|
|
558
|
+
"group_id": int(r[2]),
|
|
559
|
+
"uses": int(r[3]),
|
|
560
|
+
"duration_sec": int(r[4]),
|
|
561
|
+
}
|
|
562
|
+
)
|
|
563
|
+
except Exception:
|
|
564
|
+
continue
|
|
565
|
+
|
|
566
|
+
return out
|
|
567
|
+
except Exception:
|
|
568
|
+
return []
|
|
569
|
+
|
|
570
|
+
@db_connection
|
|
571
|
+
def validate_enrollment_code(self, *, code_name: str) -> bool:
|
|
572
|
+
# Check if enrollment code exists and has remaining uses
|
|
573
|
+
try:
|
|
574
|
+
if not isinstance(code_name, str):
|
|
575
|
+
return False
|
|
576
|
+
code_name = code_name.strip()
|
|
577
|
+
if not code_name:
|
|
578
|
+
return False
|
|
579
|
+
|
|
580
|
+
self.cursor.execute(
|
|
581
|
+
"SELECT uses FROM EnrollmentCodes WHERE code_name = ? LIMIT 1",
|
|
582
|
+
(code_name,),
|
|
583
|
+
)
|
|
584
|
+
row = self.cursor.fetchone()
|
|
585
|
+
if row is None:
|
|
586
|
+
return False
|
|
587
|
+
|
|
588
|
+
try:
|
|
589
|
+
uses_left = int(row[0])
|
|
590
|
+
except Exception:
|
|
591
|
+
return False
|
|
592
|
+
|
|
593
|
+
return uses_left > 0
|
|
594
|
+
except Exception:
|
|
595
|
+
return False
|
|
596
|
+
|
|
597
|
+
@db_connection
|
|
598
|
+
def enroll_user_with_code(self, *, uid: int, code_name: str) -> bool:
|
|
599
|
+
# Add into users table, and decrement uses in enrollment codes table
|
|
600
|
+
try:
|
|
601
|
+
uid = int(uid)
|
|
602
|
+
code_name = str(code_name).strip()
|
|
603
|
+
if uid < 0 or not code_name:
|
|
604
|
+
return False
|
|
605
|
+
|
|
606
|
+
# look up code + ensure it exists and has remaining uses
|
|
607
|
+
self.cursor.execute(
|
|
608
|
+
"SELECT group_id, uses FROM EnrollmentCodes WHERE code_name = ? LIMIT 1",
|
|
609
|
+
(code_name,),
|
|
610
|
+
)
|
|
611
|
+
row = self.cursor.fetchone()
|
|
612
|
+
if row is None:
|
|
613
|
+
return False
|
|
614
|
+
|
|
615
|
+
group_id = int(row[0])
|
|
616
|
+
uses_left = int(row[1])
|
|
617
|
+
if group_id < 0 or uses_left <= 0:
|
|
618
|
+
return False
|
|
619
|
+
|
|
620
|
+
# ensure group exists
|
|
621
|
+
self.cursor.execute("SELECT 1 FROM UserGroups WHERE group_id = ? LIMIT 1", (group_id,))
|
|
622
|
+
if self.cursor.fetchone() is None:
|
|
623
|
+
return False
|
|
624
|
+
|
|
625
|
+
# insert enrollment
|
|
626
|
+
self.cursor.execute(
|
|
627
|
+
"SELECT 1 FROM UsersLookup WHERE user_id = ? AND group_id = ? LIMIT 1",
|
|
628
|
+
(uid, group_id),
|
|
629
|
+
)
|
|
630
|
+
if self.cursor.fetchone() is None:
|
|
631
|
+
self.cursor.execute(
|
|
632
|
+
"INSERT INTO UsersLookup (user_id, group_id) VALUES (?, ?)",
|
|
633
|
+
(uid, group_id),
|
|
634
|
+
)
|
|
635
|
+
|
|
636
|
+
# decrement uses (guard again in SQL)
|
|
637
|
+
self.cursor.execute(
|
|
638
|
+
"UPDATE EnrollmentCodes SET uses = uses - 1 WHERE code_name = ? AND uses > 0",
|
|
639
|
+
(code_name,),
|
|
640
|
+
)
|
|
641
|
+
if self.cursor.rowcount <= 0:
|
|
642
|
+
return False
|
|
643
|
+
|
|
644
|
+
# delete code if exhausted
|
|
645
|
+
self.cursor.execute(
|
|
646
|
+
"DELETE FROM EnrollmentCodes WHERE code_name = ? AND uses <= 0",
|
|
647
|
+
(code_name,),
|
|
648
|
+
)
|
|
649
|
+
|
|
650
|
+
return True
|
|
651
|
+
except Exception:
|
|
652
|
+
return False
|
|
653
|
+
|
|
654
|
+
@db_connection
|
|
655
|
+
def get_users_devices(self, *, uid: int) -> list[int]:
|
|
656
|
+
# union of all devices whitelists from all groups the user is in
|
|
657
|
+
try:
|
|
658
|
+
uid = int(uid)
|
|
659
|
+
if uid < 0:
|
|
660
|
+
return []
|
|
661
|
+
|
|
662
|
+
# get all group_ids the user is enrolled in
|
|
663
|
+
self.cursor.execute(
|
|
664
|
+
"SELECT group_id FROM UsersLookup WHERE user_id = ?",
|
|
665
|
+
(uid,),
|
|
666
|
+
)
|
|
667
|
+
group_rows = self.cursor.fetchall()
|
|
668
|
+
if not group_rows:
|
|
669
|
+
return []
|
|
670
|
+
|
|
671
|
+
group_ids = [int(r[0]) for r in group_rows if r and r[0] is not None]
|
|
672
|
+
if not group_ids:
|
|
673
|
+
return []
|
|
674
|
+
|
|
675
|
+
# fetch whitelists for those groups
|
|
676
|
+
placeholders = ",".join(["?"] * len(group_ids))
|
|
677
|
+
self.cursor.execute(
|
|
678
|
+
f"SELECT devices_whitelist FROM UserGroups WHERE group_id IN ({placeholders})",
|
|
679
|
+
tuple(group_ids),
|
|
680
|
+
)
|
|
681
|
+
rows = self.cursor.fetchall()
|
|
682
|
+
if not rows:
|
|
683
|
+
return []
|
|
684
|
+
|
|
685
|
+
out_set: set[int] = set()
|
|
686
|
+
for (wl_str,) in rows:
|
|
687
|
+
if wl_str is None:
|
|
688
|
+
continue
|
|
689
|
+
try:
|
|
690
|
+
parsed = json.loads(wl_str) if isinstance(wl_str, str) else []
|
|
691
|
+
if isinstance(parsed, list):
|
|
692
|
+
for x in parsed:
|
|
693
|
+
try:
|
|
694
|
+
out_set.add(int(x))
|
|
695
|
+
except Exception:
|
|
696
|
+
pass
|
|
697
|
+
except Exception:
|
|
698
|
+
try:
|
|
699
|
+
parts = [p.strip() for p in str(wl_str).split(",")]
|
|
700
|
+
for p in parts:
|
|
701
|
+
if p != "":
|
|
702
|
+
out_set.add(int(p))
|
|
703
|
+
except Exception:
|
|
704
|
+
pass
|
|
705
|
+
|
|
706
|
+
return sorted(out_set)
|
|
707
|
+
except Exception:
|
|
708
|
+
return []
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
@db_connection
|
|
712
|
+
def get_users_max_reservation_time(self, *, uid: int, device_id: int) -> tuple[bool, int]:
|
|
713
|
+
# if the same device shows up in multiple groups, return the highest max_reservation_time_sec
|
|
714
|
+
try:
|
|
715
|
+
import json
|
|
716
|
+
|
|
717
|
+
uid = int(uid)
|
|
718
|
+
device_id = int(device_id)
|
|
719
|
+
if uid < 0 or device_id < 0:
|
|
720
|
+
return (False, 0)
|
|
721
|
+
|
|
722
|
+
# find all groups the user is in
|
|
723
|
+
self.cursor.execute("SELECT group_id FROM UsersLookup WHERE user_id = ?", (uid,))
|
|
724
|
+
group_rows = self.cursor.fetchall()
|
|
725
|
+
if not group_rows:
|
|
726
|
+
return (False, 0)
|
|
727
|
+
|
|
728
|
+
group_ids = [int(r[0]) for r in group_rows if r and r[0] is not None]
|
|
729
|
+
if not group_ids:
|
|
730
|
+
return (False, 0)
|
|
731
|
+
|
|
732
|
+
placeholders = ",".join(["?"] * len(group_ids))
|
|
733
|
+
self.cursor.execute(
|
|
734
|
+
f"SELECT devices_whitelist, max_reservation_time_sec FROM UserGroups WHERE group_id IN ({placeholders})",
|
|
735
|
+
tuple(group_ids),
|
|
736
|
+
)
|
|
737
|
+
rows = self.cursor.fetchall()
|
|
738
|
+
if not rows:
|
|
739
|
+
return (False, 0)
|
|
740
|
+
|
|
741
|
+
allowed = False
|
|
742
|
+
best = 0
|
|
743
|
+
|
|
744
|
+
for wl_str, max_time in rows:
|
|
745
|
+
# parse whitelist (JSON array preferred, tolerate fallback)
|
|
746
|
+
whitelist: list[int] = []
|
|
747
|
+
if wl_str is not None:
|
|
748
|
+
try:
|
|
749
|
+
parsed = json.loads(wl_str) if isinstance(wl_str, str) else []
|
|
750
|
+
if isinstance(parsed, list):
|
|
751
|
+
whitelist = [int(x) for x in parsed if str(x).strip() != ""]
|
|
752
|
+
except Exception:
|
|
753
|
+
try:
|
|
754
|
+
whitelist = [int(p.strip()) for p in str(wl_str).split(",") if p.strip() != ""]
|
|
755
|
+
except Exception:
|
|
756
|
+
whitelist = []
|
|
757
|
+
|
|
758
|
+
if device_id in whitelist:
|
|
759
|
+
allowed = True
|
|
760
|
+
try:
|
|
761
|
+
t = int(max_time)
|
|
762
|
+
except Exception:
|
|
763
|
+
t = 0
|
|
764
|
+
if t > best:
|
|
765
|
+
best = t
|
|
766
|
+
|
|
767
|
+
return (allowed, best if allowed else 0)
|
|
768
|
+
except Exception:
|
|
769
|
+
return (False, 0)
|
|
770
|
+
|
|
771
|
+
@db_connection
|
|
772
|
+
def get_users_max_reservations(self, *, uid: int, device_id: int) -> tuple[bool, int]:
|
|
773
|
+
# if the same device shows up in multiple groups, return the highest max_reservations
|
|
774
|
+
try:
|
|
775
|
+
import json
|
|
776
|
+
|
|
777
|
+
uid = int(uid)
|
|
778
|
+
device_id = int(device_id)
|
|
779
|
+
if uid < 0 or device_id < 0:
|
|
780
|
+
return (False, 0)
|
|
781
|
+
|
|
782
|
+
# find all groups the user is in
|
|
783
|
+
self.cursor.execute("SELECT group_id FROM UsersLookup WHERE user_id = ?", (uid,))
|
|
784
|
+
group_rows = self.cursor.fetchall()
|
|
785
|
+
if not group_rows:
|
|
786
|
+
return (False, 0)
|
|
787
|
+
|
|
788
|
+
group_ids = [int(r[0]) for r in group_rows if r and r[0] is not None]
|
|
789
|
+
if not group_ids:
|
|
790
|
+
return (False, 0)
|
|
791
|
+
|
|
792
|
+
placeholders = ",".join(["?"] * len(group_ids))
|
|
793
|
+
self.cursor.execute(
|
|
794
|
+
f"SELECT devices_whitelist, max_reservations FROM UserGroups WHERE group_id IN ({placeholders})",
|
|
795
|
+
tuple(group_ids),
|
|
796
|
+
)
|
|
797
|
+
rows = self.cursor.fetchall()
|
|
798
|
+
if not rows:
|
|
799
|
+
return (False, 0)
|
|
800
|
+
|
|
801
|
+
allowed = False
|
|
802
|
+
best = 0
|
|
803
|
+
|
|
804
|
+
for wl_str, max_res in rows:
|
|
805
|
+
# parse whitelist (JSON array preferred, tolerate fallback)
|
|
806
|
+
whitelist: list[int] = []
|
|
807
|
+
if wl_str is not None:
|
|
808
|
+
try:
|
|
809
|
+
parsed = json.loads(wl_str) if isinstance(wl_str, str) else []
|
|
810
|
+
if isinstance(parsed, list):
|
|
811
|
+
whitelist = [int(x) for x in parsed if str(x).strip() != ""]
|
|
812
|
+
except Exception:
|
|
813
|
+
try:
|
|
814
|
+
whitelist = [int(p.strip()) for p in str(wl_str).split(",") if p.strip() != ""]
|
|
815
|
+
except Exception:
|
|
816
|
+
whitelist = []
|
|
817
|
+
|
|
818
|
+
if device_id in whitelist:
|
|
819
|
+
allowed = True
|
|
820
|
+
try:
|
|
821
|
+
n = int(max_res)
|
|
822
|
+
except Exception:
|
|
823
|
+
n = 0
|
|
824
|
+
if n > best:
|
|
825
|
+
best = n
|
|
826
|
+
|
|
827
|
+
return (allowed, best if allowed else 0)
|
|
828
|
+
except Exception:
|
|
829
|
+
return (False, 0)
|
|
830
|
+
|
|
831
|
+
def _parse_whitelist(self, wl_str) -> set[int]:
|
|
832
|
+
try:
|
|
833
|
+
if wl_str is None:
|
|
834
|
+
return set()
|
|
835
|
+
if isinstance(wl_str, str):
|
|
836
|
+
parsed = json.loads(wl_str)
|
|
837
|
+
if isinstance(parsed, list):
|
|
838
|
+
return {int(x) for x in parsed}
|
|
839
|
+
return set()
|
|
840
|
+
except Exception:
|
|
841
|
+
# tolerate old formats like "1,2,3"
|
|
842
|
+
try:
|
|
843
|
+
return {int(p.strip()) for p in str(wl_str).split(",") if p.strip() != ""}
|
|
844
|
+
except Exception:
|
|
845
|
+
return set()
|
|
846
|
+
|
|
847
|
+
@db_connection
|
|
848
|
+
def get_user_group_reservation_quota_rows(
|
|
849
|
+
self,
|
|
850
|
+
*,
|
|
851
|
+
uid: int,
|
|
852
|
+
reserved_device_ids: list[int],
|
|
853
|
+
) -> list[list[int]]:
|
|
854
|
+
"""
|
|
855
|
+
Returns rows: [group_id, used_reservations_in_group, max_reservations_in_group]
|
|
856
|
+
for every group the user is in.
|
|
857
|
+
"""
|
|
858
|
+
try:
|
|
859
|
+
uid = int(uid)
|
|
860
|
+
if uid < 0:
|
|
861
|
+
return []
|
|
862
|
+
|
|
863
|
+
# sanitize reserved_device_ids
|
|
864
|
+
try:
|
|
865
|
+
rdevs = [int(d) for d in (reserved_device_ids or [])]
|
|
866
|
+
except Exception:
|
|
867
|
+
rdevs = []
|
|
868
|
+
|
|
869
|
+
# groups for user
|
|
870
|
+
self.cursor.execute("SELECT group_id FROM UsersLookup WHERE user_id = ?", (uid,))
|
|
871
|
+
group_rows = self.cursor.fetchall() or []
|
|
872
|
+
group_ids = [int(r[0]) for r in group_rows if r and r[0] is not None]
|
|
873
|
+
if not group_ids:
|
|
874
|
+
return []
|
|
875
|
+
|
|
876
|
+
placeholders = ",".join(["?"] * len(group_ids))
|
|
877
|
+
self.cursor.execute(
|
|
878
|
+
f"""
|
|
879
|
+
SELECT group_id, devices_whitelist, max_reservations
|
|
880
|
+
FROM UserGroups
|
|
881
|
+
WHERE group_id IN ({placeholders})
|
|
882
|
+
""",
|
|
883
|
+
tuple(group_ids),
|
|
884
|
+
)
|
|
885
|
+
rows = self.cursor.fetchall() or []
|
|
886
|
+
|
|
887
|
+
out: list[list[int]] = []
|
|
888
|
+
for gid, wl_str, max_res in rows:
|
|
889
|
+
try:
|
|
890
|
+
gid = int(gid)
|
|
891
|
+
max_res = int(max_res)
|
|
892
|
+
except Exception:
|
|
893
|
+
continue
|
|
894
|
+
|
|
895
|
+
wl = self._parse_whitelist(wl_str)
|
|
896
|
+
used = 0
|
|
897
|
+
for d in rdevs:
|
|
898
|
+
if d in wl:
|
|
899
|
+
used += 1
|
|
900
|
+
|
|
901
|
+
out.append([gid, used, max_res])
|
|
902
|
+
|
|
903
|
+
# stable order
|
|
904
|
+
out.sort(key=lambda x: x[0])
|
|
905
|
+
return out
|
|
906
|
+
except Exception:
|
|
907
|
+
return []
|
|
908
|
+
|
|
909
|
+
@db_connection
|
|
910
|
+
def get_group_quota_for_device(
|
|
911
|
+
self,
|
|
912
|
+
*,
|
|
913
|
+
uid: int,
|
|
914
|
+
device_id: int,
|
|
915
|
+
reserved_device_ids: list[int],
|
|
916
|
+
) -> tuple[bool, int, int, int]:
|
|
917
|
+
try:
|
|
918
|
+
uid = int(uid)
|
|
919
|
+
device_id = int(device_id)
|
|
920
|
+
if uid < 0 or device_id < 0:
|
|
921
|
+
return (False, 0, 0, -1)
|
|
922
|
+
|
|
923
|
+
try:
|
|
924
|
+
rdevs = [int(d) for d in (reserved_device_ids or [])]
|
|
925
|
+
except Exception:
|
|
926
|
+
rdevs = []
|
|
927
|
+
|
|
928
|
+
self.cursor.execute("SELECT group_id FROM UsersLookup WHERE user_id = ?", (uid,))
|
|
929
|
+
group_rows = self.cursor.fetchall() or []
|
|
930
|
+
group_ids = [int(r[0]) for r in group_rows if r and r[0] is not None]
|
|
931
|
+
if not group_ids:
|
|
932
|
+
return (False, 0, 0, -1)
|
|
933
|
+
|
|
934
|
+
placeholders = ",".join(["?"] * len(group_ids))
|
|
935
|
+
self.cursor.execute(
|
|
936
|
+
f"""
|
|
937
|
+
SELECT group_id, devices_whitelist, max_reservations
|
|
938
|
+
FROM UserGroups
|
|
939
|
+
WHERE group_id IN ({placeholders})
|
|
940
|
+
""",
|
|
941
|
+
tuple(group_ids),
|
|
942
|
+
)
|
|
943
|
+
rows = self.cursor.fetchall() or []
|
|
944
|
+
if not rows:
|
|
945
|
+
return (False, 0, 0, -1)
|
|
946
|
+
|
|
947
|
+
candidates = []
|
|
948
|
+
for gid, wl_str, max_res in rows:
|
|
949
|
+
try:
|
|
950
|
+
gid = int(gid)
|
|
951
|
+
max_res = int(max_res)
|
|
952
|
+
except Exception:
|
|
953
|
+
continue
|
|
954
|
+
|
|
955
|
+
wl = self._parse_whitelist(wl_str)
|
|
956
|
+
if device_id not in wl:
|
|
957
|
+
continue
|
|
958
|
+
|
|
959
|
+
used = 0
|
|
960
|
+
for d in rdevs:
|
|
961
|
+
if d in wl:
|
|
962
|
+
used += 1
|
|
963
|
+
|
|
964
|
+
remaining = max_res - used # can be <= 0 (meaning full/exceeded)
|
|
965
|
+
|
|
966
|
+
# Sort key: prefer more remaining, then higher max, then fewer used, then lower gid
|
|
967
|
+
candidates.append((remaining, max_res, -used, -gid, used, gid))
|
|
968
|
+
|
|
969
|
+
if not candidates:
|
|
970
|
+
return (False, 0, 0, -1)
|
|
971
|
+
|
|
972
|
+
candidates.sort(reverse=True)
|
|
973
|
+
_remaining, max_res, _neg_used, _neg_gid, used, gid = candidates[0]
|
|
974
|
+
return (True, used, max_res, gid)
|
|
975
|
+
|
|
976
|
+
except Exception:
|
|
977
|
+
return (False, 0, 0, -1)
|
|
978
|
+
|
|
979
|
+
@db_connection
|
|
980
|
+
def check_expiration_and_cleanup(self) -> dict:
|
|
981
|
+
def _parse_dt(v):
|
|
982
|
+
if v is None:
|
|
983
|
+
return None
|
|
984
|
+
if isinstance(v, datetime):
|
|
985
|
+
return v
|
|
986
|
+
s = str(v).strip()
|
|
987
|
+
if not s:
|
|
988
|
+
return None
|
|
989
|
+
try:
|
|
990
|
+
return datetime.fromisoformat(s)
|
|
991
|
+
except Exception:
|
|
992
|
+
pass
|
|
993
|
+
for fmt in ("%Y-%m-%d %H:%M:%S", "%Y-%m-%d %H:%M:%S.%f"):
|
|
994
|
+
try:
|
|
995
|
+
return datetime.strptime(s, fmt)
|
|
996
|
+
except Exception:
|
|
997
|
+
continue
|
|
998
|
+
return None
|
|
999
|
+
|
|
1000
|
+
try:
|
|
1001
|
+
now = datetime.now()
|
|
1002
|
+
|
|
1003
|
+
print("Flushing User Groups")
|
|
1004
|
+
|
|
1005
|
+
expired_group_ids: list[int] = []
|
|
1006
|
+
expired_code_names: list[str] = []
|
|
1007
|
+
|
|
1008
|
+
# ---- find expired groups ----
|
|
1009
|
+
self.cursor.execute("""
|
|
1010
|
+
SELECT group_id, lifetime_sec, created_at
|
|
1011
|
+
FROM UserGroups
|
|
1012
|
+
WHERE lifetime_sec > 0
|
|
1013
|
+
""")
|
|
1014
|
+
for group_id, lifetime_sec, created_at in (self.cursor.fetchall() or []):
|
|
1015
|
+
try:
|
|
1016
|
+
gid = int(group_id)
|
|
1017
|
+
life = int(lifetime_sec)
|
|
1018
|
+
except Exception:
|
|
1019
|
+
continue
|
|
1020
|
+
if gid <= 0 or life <= 0:
|
|
1021
|
+
continue
|
|
1022
|
+
|
|
1023
|
+
dt = _parse_dt(created_at)
|
|
1024
|
+
if dt is None:
|
|
1025
|
+
continue
|
|
1026
|
+
|
|
1027
|
+
if (now - dt).total_seconds() >= life:
|
|
1028
|
+
expired_group_ids.append(gid)
|
|
1029
|
+
|
|
1030
|
+
# ---- find expired codes ----
|
|
1031
|
+
self.cursor.execute("""
|
|
1032
|
+
SELECT code_name, duration_sec, created_at
|
|
1033
|
+
FROM EnrollmentCodes
|
|
1034
|
+
WHERE duration_sec > 0
|
|
1035
|
+
""")
|
|
1036
|
+
for code_name, duration_sec, created_at in (self.cursor.fetchall() or []):
|
|
1037
|
+
code = str(code_name).strip() if code_name is not None else ""
|
|
1038
|
+
try:
|
|
1039
|
+
dur = int(duration_sec)
|
|
1040
|
+
except Exception:
|
|
1041
|
+
continue
|
|
1042
|
+
if not code or dur <= 0:
|
|
1043
|
+
continue
|
|
1044
|
+
|
|
1045
|
+
dt = _parse_dt(created_at)
|
|
1046
|
+
if dt is None:
|
|
1047
|
+
continue
|
|
1048
|
+
|
|
1049
|
+
if (now - dt).total_seconds() >= dur:
|
|
1050
|
+
expired_code_names.append(code)
|
|
1051
|
+
|
|
1052
|
+
# De-dupe
|
|
1053
|
+
expired_group_ids = sorted(set(expired_group_ids))
|
|
1054
|
+
expired_code_names = sorted(set(expired_code_names))
|
|
1055
|
+
|
|
1056
|
+
codes_deleted = 0
|
|
1057
|
+
groups_deleted = 0
|
|
1058
|
+
|
|
1059
|
+
# ---- delete expired codes by name ----
|
|
1060
|
+
for code in expired_code_names:
|
|
1061
|
+
try:
|
|
1062
|
+
# reuse your existing deletion logic, but inline is cheaper
|
|
1063
|
+
self.cursor.execute("DELETE FROM EnrollmentCodes WHERE code_name = ?", (code,))
|
|
1064
|
+
if self.cursor.rowcount > 0:
|
|
1065
|
+
codes_deleted += 1
|
|
1066
|
+
except Exception:
|
|
1067
|
+
continue
|
|
1068
|
+
|
|
1069
|
+
# ---- delete expired groups using your existing logic (cleanup cascade) ----
|
|
1070
|
+
for gid in expired_group_ids:
|
|
1071
|
+
try:
|
|
1072
|
+
# Call the existing method; it will be serialized anyway, but avoid re-entering wrapper.
|
|
1073
|
+
# So do the same SQL your delete_user_group() does:
|
|
1074
|
+
self.cursor.execute("DELETE FROM EnrollmentCodes WHERE group_id = ?", (gid,))
|
|
1075
|
+
self.cursor.execute("DELETE FROM UsersLookup WHERE group_id = ?", (gid,))
|
|
1076
|
+
self.cursor.execute("DELETE FROM UserGroups WHERE group_id = ?", (gid,))
|
|
1077
|
+
if self.cursor.rowcount > 0:
|
|
1078
|
+
groups_deleted += 1
|
|
1079
|
+
except Exception:
|
|
1080
|
+
continue
|
|
1081
|
+
|
|
1082
|
+
return {
|
|
1083
|
+
"expired_groups_deleted": groups_deleted,
|
|
1084
|
+
"expired_codes_deleted": codes_deleted,
|
|
1085
|
+
"expired_group_ids": expired_group_ids,
|
|
1086
|
+
"expired_code_names": expired_code_names,
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1089
|
+
except Exception:
|
|
1090
|
+
return {
|
|
1091
|
+
"expired_groups_deleted": 0,
|
|
1092
|
+
"expired_codes_deleted": 0,
|
|
1093
|
+
"expired_group_ids": [],
|
|
1094
|
+
"expired_code_names": [],
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
user_group_handler = UserGroupHandler()
|
|
1098
|
+
|
|
1099
|
+
def start_usergroup_cleanup_loop(
|
|
1100
|
+
*,
|
|
1101
|
+
interval_sec: int,
|
|
1102
|
+
) -> Tuple[threading.Event, threading.Thread]:
|
|
1103
|
+
interval_sec = int(interval_sec)
|
|
1104
|
+
if interval_sec <= 0:
|
|
1105
|
+
interval_sec = 60
|
|
1106
|
+
|
|
1107
|
+
stop_event = threading.Event()
|
|
1108
|
+
|
|
1109
|
+
def _worker():
|
|
1110
|
+
while not stop_event.is_set():
|
|
1111
|
+
try:
|
|
1112
|
+
user_group_handler.check_expiration_and_cleanup()
|
|
1113
|
+
except Exception:
|
|
1114
|
+
# optional: log/printf traceback here if you want
|
|
1115
|
+
pass
|
|
1116
|
+
|
|
1117
|
+
# interruptible sleep (wakes immediately when stop_event.set() is called)
|
|
1118
|
+
stop_event.wait(interval_sec)
|
|
1119
|
+
|
|
1120
|
+
threading.Thread(target=_worker, daemon=True)
|