gitscope
← marzneshin / nightly / app/routes/user.py
https://dev.kronoz.win/r/marzneshin.git
app/routes/user.py · Python
Blame Raw
  1import asyncio
  2import logging
  3from datetime import datetime, timedelta
  4from enum import StrEnum
  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(StrEnum):
 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