1import asyncio
2import logging
3from datetime import datetime, timedelta
4from enum import Enum
5
6import sqlalchemy
7from fastapi import APIRouter
8from fastapi import HTTPException, Query
9from fastapi_pagination.ext.sqlalchemy import paginate
10from fastapi_pagination.links import Page
11
12from app import marznode
13from app.db import crud, User
14from app.db.models import Service
15from app.dependencies import (
16 DBDep,
17 AdminDep,
18 SudoAdminDep,
19 UserDep,
20 StartDateDep,
21 EndDateDep,
22 ModifyUsersAccess,
23)
24from app.models.notification import UserNotification
25from app.models.service import ServiceResponse
26from app.models.user import (
27 UserCreate,
28 UserModify,
29 UserResponse,
30 UserUsageSeriesResponse,
31)
32from app.notification import notify
33
34logger = logging.getLogger(__name__)
35router = APIRouter(prefix="/users", tags=["User"])
36
37
38class UsersSortingOptions(str, Enum):
39 USERNAME = "username"
40 USED_TRAFFIC = "used_traffic"
41 DATA_LIMIT = "data_limit"
42 EXPIRE_DATE = "expire_date"
43 CREATED_AT = "created_at"
44
45
46user_filters = [
47 "username",
48 "is_active",
49 "activated",
50 "expired",
51 "data_limit_reached",
52 "enabled",
53 "owner_username",
54]
55
56
57@router.get("", response_model=Page[UserResponse])
58def get_users(
59 db: DBDep,
60 admin: AdminDep,
61 username: list[str] = Query(None),
62 order_by: UsersSortingOptions = Query(None),
63 descending: bool = Query(False),
64 is_active: bool | None = Query(None),
65 activated: bool | None = Query(None),
66 expired: bool | None = Query(None),
67 data_limit_reached: bool | None = Query(None),
68 enabled: bool | None = Query(None),
69 owner_username: str | None = Query(None),
70):
71 """
72 Filters users based on the options
73 """
74 dbadmin = crud.get_admin(db, admin.username)
75
76 query = db.query(User).filter(User.removed == False)
77
78 admin = dbadmin if not admin.is_sudo else None
79
80 for name in user_filters:
81 value = locals().get(name)
82 if value is not None:
83 if name == "username":
84 if len(username) > 1:
85 query = query.filter(User.username.in_(username))
86 else:
87 query = query.filter(
88 User.username.ilike(f"%{username[0]}%")
89 )
90 elif name == "owner_username":
91 if not dbadmin.is_sudo:
92 raise HTTPException(403, "You're not allowed.")
93 filter_admin = crud.get_admin(db, value)
94 if not filter_admin:
95 raise HTTPException(404, "owner_username not found.")
96 query = query.filter(User.admin_id == filter_admin.id)
97 else:
98 query = query.filter(getattr(User, name) == value)
99
100 if admin:
101 query = query.filter(User.admin == admin)
102
103 if order_by:
104 order_column = getattr(User, order_by)
105 if descending:
106 order_column = order_column.desc()
107 query = query.order_by(order_column)
108
109 return paginate(db, query)
110
111
112@router.post("", response_model=UserResponse)
113async def add_user(new_user: UserCreate, db: DBDep, admin: AdminDep):
114 """
115 Add a new user
116
117 - **username** must have 3 to 32 characters and is allowed to contain a-z, 0-9, and underscores in between
118 - **expire_date** must be a datetime
119 - **data_limit** must be in Bytes, e.g. 1073741824B = 1GB
120 - **services** list of service ids
121 """
122
123 try:
124 db_user = crud.create_user(
125 db,
126 new_user,
127 admin=crud.get_admin(db, admin.username),
128 allowed_services=(
129 admin.service_ids
130 if not admin.is_sudo and not admin.all_services_access
131 else None
132 ),
133 )
134 except sqlalchemy.exc.IntegrityError:
135 db.rollback()
136 raise HTTPException(status_code=409, detail="User already exists")
137
138 user = UserResponse.model_validate(db_user)
139 marznode.operations.update_user(user=db_user)
140
141 asyncio.ensure_future(
142 notify(
143 action=UserNotification.Action.user_created, user=user, by=admin
144 )
145 )
146
147 logger.info("New user `%s` added", db_user.username)
148 return user
149
150
151@router.post("/reset")
152async def reset_users_data_usage(db: DBDep, admin: SudoAdminDep):
153 """
154 Reset all users data usage,
155 You will need to restart for this to take effect for now
156 """
157 dbadmin = crud.get_admin(db, admin.username)
158 crud.reset_all_users_data_usage(db=db, admin=dbadmin)
159 return {}
160
161
162@router.delete("/expired")
163async def delete_expired(
164 passed_time: int,
165 db: DBDep,
166 admin: AdminDep,
167 modify_access: ModifyUsersAccess,
168):
169 """
170 Delete expired users
171 - **passed_time** must be a timestamp
172 - This function will delete all expired users that meet the specified number of days passed and can't be undone.
173 """
174
175 dbadmin = crud.get_admin(db, admin.username)
176
177 db_users = crud.get_users(
178 db=db,
179 expired=True,
180 admin=dbadmin if not admin.is_sudo else None,
181 )
182
183 current_time = datetime.utcnow()
184 expiration_threshold = current_time - timedelta(seconds=passed_time)
185 expired_users = [
186 user
187 for user in db_users
188 if user.expire_date is not None
189 and user.expire_date <= expiration_threshold
190 ]
191 if not expired_users:
192 raise HTTPException(status_code=404, detail="No expired user found.")
193
194 for db_user in expired_users:
195 crud.remove_user(db, db_user)
196
197 logger.info("User `%s` removed", db_user.username)
198
199 return {}
200
201
202@router.get("/{username}", response_model=UserResponse)
203def get_user(db_user: UserDep):
204 """
205 Get users information
206 """
207 return db_user
208
209
210@router.put("/{username}", response_model=UserResponse)
211async def modify_user(
212 db_user: UserDep,
213 modifications: UserModify,
214 db: DBDep,
215 admin: AdminDep,
216 modify_access: ModifyUsersAccess,
217):
218 """
219 Modify a user
220
221 - set **data_limit** to 0 to make the user unlimited in data, null for no change
222 """
223 active_before = db_user.is_active
224
225 old_inbounds = {(i.node_id, i.protocol, i.tag) for i in db_user.inbounds}
226 new_user = crud.update_user(
227 db,
228 db_user,
229 modifications,
230 allowed_services=(
231 admin.service_ids
232 if not admin.is_sudo and not admin.all_services_access
233 else None
234 ),
235 )
236 active_after = new_user.is_active
237 new_inbounds = {(i.node_id, i.protocol, i.tag) for i in new_user.inbounds}
238
239 inbound_change = old_inbounds != new_inbounds
240
241 if (
242 inbound_change and new_user.is_active
243 ) or active_before != active_after:
244 marznode.operations.update_user(
245 new_user, old_inbounds, remove=not db_user.is_active
246 )
247 db_user.activated = db_user.is_active
248 db.commit()
249
250 asyncio.ensure_future(
251 notify(
252 action=UserNotification.Action.user_updated,
253 user=UserResponse.model_validate(db_user),
254 by=admin,
255 )
256 )
257
258 logger.info("User `%s` modified", db_user.username)
259
260 if active_before != active_after:
261 action = (
262 UserNotification.Action.user_activated
263 if active_after
264 else UserNotification.Action.user_deactivated
265 )
266
267 asyncio.ensure_future(
268 notify(
269 action=action,
270 user=UserResponse.model_validate(db_user),
271 by=admin,
272 )
273 )
274
275 logger.info(
276 "User `%s` activation changed from `%s` to `%s`",
277 db_user.username,
278 active_before,
279 active_after,
280 )
281
282 return db_user
283
284
285@router.delete("/{username}")
286async def remove_user(
287 db_user: UserDep,
288 db: DBDep,
289 admin: AdminDep,
290 modify_access: ModifyUsersAccess,
291):
292 """
293 Remove a user
294 """
295 marznode.operations.update_user(db_user, remove=True)
296
297 deleted_username = db_user.username
298 crud.remove_user(db, db_user)
299
300 db_user.username = deleted_username
301 user = UserResponse.model_validate(db_user)
302 db.expunge(db_user)
303
304 asyncio.ensure_future(
305 notify(
306 action=UserNotification.Action.user_deleted, user=user, by=admin
307 )
308 )
309
310 logger.info("User %s deleted", db_user.username)
311 return {}
312
313
314@router.get("/{username}/services", response_model=Page[ServiceResponse])
315def get_user_services(user: UserDep, db: DBDep, admin: AdminDep):
316 """
317 Get user services
318 """
319
320 query = (
321 db.query(Service)
322 .join(Service.users)
323 .where(User.username == user.username)
324 )
325
326 if not admin.is_sudo and not admin.all_services_access:
327 query.filter(Service.id.in_(admin.service_ids))
328
329 return paginate(query)
330
331
332@router.post("/{username}/reset", response_model=UserResponse)
333async def reset_user_data_usage(
334 db_user: UserDep,
335 db: DBDep,
336 admin: AdminDep,
337 modify_access: ModifyUsersAccess,
338):
339 """
340 Reset user data usage
341 """
342 was_active = db_user.is_active
343 db_user = crud.reset_user_data_usage(db, db_user)
344
345 if db_user.is_active and not was_active:
346 marznode.operations.update_user(db_user)
347 db_user.activated = True
348 db.commit()
349
350 user = UserResponse.model_validate(db_user)
351
352 asyncio.ensure_future(
353 notify(
354 action=UserNotification.Action.data_usage_reset,
355 user=user,
356 by=admin,
357 )
358 )
359
360 logger.info("User `%s`'s usage was reset", db_user.username)
361
362 return user
363
364
365@router.post("/{username}/enable", response_model=UserResponse)
366async def enable_user(
367 db_user: UserDep,
368 db: DBDep,
369 admin: AdminDep,
370 modify_access: ModifyUsersAccess,
371):
372 """
373 Enables a user
374 """
375 if db_user.enabled:
376 raise HTTPException(409, "User is already enabled")
377
378 db_user.enabled = True
379
380 if db_user.is_active:
381 db_user.activated = True
382 marznode.operations.update_user(db_user)
383
384 db.commit()
385
386 user = UserResponse.model_validate(db_user)
387
388 asyncio.ensure_future(
389 notify(
390 action=UserNotification.Action.user_enabled, user=user, by=admin
391 )
392 )
393
394 logger.info("User `%s` has been enabled", db_user.username)
395
396 return user
397
398
399@router.post("/{username}/disable", response_model=UserResponse)
400async def disable_user(
401 db_user: UserDep,
402 db: DBDep,
403 admin: AdminDep,
404 modify_access: ModifyUsersAccess,
405):
406 """
407 Disables a user
408 """
409 if not db_user.enabled:
410 raise HTTPException(409, "User is not enabled")
411 db_user.enabled = False
412 db_user.activated = False
413 db.commit()
414
415 marznode.operations.update_user(db_user, remove=True)
416
417 user = UserResponse.model_validate(db_user)
418
419 asyncio.ensure_future(
420 notify(
421 action=UserNotification.Action.user_disabled, user=user, by=admin
422 )
423 )
424
425 logger.info("User `%s` has been disabled", db_user.username)
426
427 return user
428
429
430@router.post("/{username}/revoke_sub", response_model=UserResponse)
431async def revoke_user_subscription(
432 db_user: UserDep,
433 db: DBDep,
434 admin: AdminDep,
435 modify_access: ModifyUsersAccess,
436):
437 """
438 Revoke users subscription (Subscription link and proxies)
439 """
440 db_user = crud.revoke_user_sub(db, db_user)
441
442 if db_user.is_active:
443 marznode.operations.update_user(db_user, remove=True)
444 marznode.operations.update_user(db_user)
445
446 user = UserResponse.model_validate(db_user)
447
448 asyncio.ensure_future(
449 notify(
450 action=UserNotification.Action.subscription_revoked,
451 user=user,
452 by=admin,
453 )
454 )
455
456 logger.info("User %s subscription revoked", db_user.username)
457
458 return user
459
460
461@router.get("/{username}/usage", response_model=UserUsageSeriesResponse)
462def get_user_usage(
463 db: DBDep, db_user: UserDep, start_date: StartDateDep, end_date: EndDateDep
464):
465 """
466 Get users usage
467 """
468
469 return crud.get_user_usages(db, db_user, start_date, end_date)
470
471
472@router.put("/{username}/set-owner", response_model=UserResponse)
473def set_owner(
474 username: str, admin_username: str, db: DBDep, admin: SudoAdminDep
475):
476 db_user = crud.get_user(db, username)
477 if not db_user:
478 raise HTTPException(status_code=404, detail="User not found")
479
480 new_admin = crud.get_admin(db, username=admin_username)
481 if not new_admin:
482 raise HTTPException(status_code=404, detail="Admin not found")
483
484 db_user = crud.set_owner(db, db_user, new_admin)
485 user = UserResponse.model_validate(db_user)
486
487 logger.info(
488 "`%s`'s owner successfully set to `%s`", user.username, admin.username
489 )
490
491 return user