files-server-fastapi 0.1.45__tar.gz → 0.1.46__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.
Files changed (42) hide show
  1. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/PKG-INFO +1 -1
  2. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/pyproject.toml +1 -1
  3. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/acls_router.py +9 -8
  4. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/create_file_router.py +3 -2
  5. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/delete_router.py +3 -2
  6. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/dependencies.py +3 -2
  7. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/folder_router.py +3 -2
  8. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/list_router.py +3 -2
  9. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/open_url_router.py +3 -2
  10. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/search_router.py +4 -3
  11. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/upload_router.py +3 -2
  12. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/models/users_extend_model.py +7 -2
  13. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/routers/area_router.py +6 -5
  14. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/routers/permisos_router.py +9 -8
  15. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/routers/rol_router.py +3 -2
  16. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/routers/rutas_router.py +3 -2
  17. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/routers/users_extend_router.py +42 -15
  18. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/uv.lock +1 -1
  19. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/.gitignore +0 -0
  20. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/.python-version +0 -0
  21. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/LICENSE +0 -0
  22. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/MANIFEST.in +0 -0
  23. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/README.md +0 -0
  24. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/__init__.py +0 -0
  25. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/dependencies/__init__.py +0 -0
  26. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/dependencies/user_dependencies.py +0 -0
  27. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/__init__.py +0 -0
  28. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/constants.py +0 -0
  29. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/download_router.py +0 -0
  30. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/onlyoffice_router.py +0 -0
  31. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/path_utils.py +0 -0
  32. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/stats_router.py +0 -0
  33. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/tree_router.py +0 -0
  34. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/files/view_router.py +0 -0
  35. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/models/__init__.py +0 -0
  36. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/models/area_model.py +0 -0
  37. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/models/permisos_model.py +0 -0
  38. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/models/rol_model.py +0 -0
  39. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/models/rutas_model.py +0 -0
  40. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/routers/__init__.py +0 -0
  41. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/routers/files_router.py +0 -0
  42. {files_server_fastapi-0.1.45 → files_server_fastapi-0.1.46}/src/files_server_fastapi/routers/samba_router.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: files-server-fastapi
3
- Version: 0.1.45
3
+ Version: 0.1.46
4
4
  Summary: Este repositorio es un paquete python desarrollado con FastAPI, gestiona la comunicación con un servidor Samba y utiliza PostgreSQL para administrar rutas de archivos y el control de acceso basado en roles (RBAC) de los usuarios.
5
5
  Project-URL: Homepage, https://github.com/AldoBP/files-server-fastapi
6
6
  Project-URL: Repository, https://github.com/AldoBP/files-server-fastapi
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "files-server-fastapi"
3
- version = "0.1.45"
3
+ version = "0.1.46"
4
4
  description = "Este repositorio es un paquete python desarrollado con FastAPI, gestiona la comunicación con un servidor Samba y utiliza PostgreSQL para administrar rutas de archivos y el control de acceso basado en roles (RBAC) de los usuarios."
5
5
  readme = "README.md"
6
6
  license = { file = "LICENSE" }
@@ -7,7 +7,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
7
7
  from sqlalchemy import select, delete, or_
8
8
 
9
9
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
10
- from oauth2fast_fastapi import get_current_verified_user, User
10
+ from oauth2fast_fastapi import User
11
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
11
12
  from files_server_fastapi.files.path_utils import normalize_subpath, build_logical_path
12
13
  from files_server_fastapi.models.permisos_model import User_Ruta_Access, Permisos
13
14
  from files_server_fastapi.models.rutas_model import Rutas
@@ -90,7 +91,7 @@ class AclCreate(BaseModel):
90
91
  @router.post("/acls", summary="Asignar acceso a una carpeta específica (ACL)")
91
92
  async def create_acl(
92
93
  req: AclCreate,
93
- current_user: User = Depends(get_current_verified_user),
94
+ current_user: User = Depends(get_active_user),
94
95
  db: AsyncSession = Depends(get_db_session)
95
96
  ):
96
97
  """
@@ -212,7 +213,7 @@ async def create_acl(
212
213
 
213
214
  @router.get("/acls", summary="Obtener las carpetas compartidas del usuario")
214
215
  async def get_user_acls(
215
- current_user: User = Depends(get_current_verified_user),
216
+ current_user: User = Depends(get_active_user),
216
217
  db: AsyncSession = Depends(get_db_session)
217
218
  ):
218
219
  """
@@ -257,7 +258,7 @@ async def get_user_acls(
257
258
  @router.get("/acls/user/{user_id}", summary="Obtener los ACLs de un usuario específico")
258
259
  async def get_specific_user_acls(
259
260
  user_id: int,
260
- current_user: User = Depends(get_current_verified_user),
261
+ current_user: User = Depends(get_active_user),
261
262
  db: AsyncSession = Depends(get_db_session),
262
263
  ):
263
264
  """
@@ -321,7 +322,7 @@ async def get_specific_user_acls(
321
322
  async def initialize_user_acl(
322
323
  user_id: int,
323
324
  grant_full_area: bool = False,
324
- current_user: User = Depends(get_current_verified_user),
325
+ current_user: User = Depends(get_active_user),
325
326
  db: AsyncSession = Depends(get_db_session),
326
327
  ):
327
328
  """
@@ -403,7 +404,7 @@ async def initialize_user_acl(
403
404
  )
404
405
  async def grant_full_area(
405
406
  user_id: int,
406
- current_user: User = Depends(get_current_verified_user),
407
+ current_user: User = Depends(get_active_user),
407
408
  db: AsyncSession = Depends(get_db_session),
408
409
  ):
409
410
  """
@@ -426,7 +427,7 @@ async def grant_full_area(
426
427
  )
427
428
  async def revoke_full_area(
428
429
  user_id: int,
429
- current_user: User = Depends(get_current_verified_user),
430
+ current_user: User = Depends(get_active_user),
430
431
  db: AsyncSession = Depends(get_db_session),
431
432
  ):
432
433
  """
@@ -487,7 +488,7 @@ async def revoke_full_area(
487
488
  async def delete_user_acl(
488
489
  user_id: int,
489
490
  ruta_id: int,
490
- current_user: User = Depends(get_current_verified_user),
491
+ current_user: User = Depends(get_active_user),
491
492
  db: AsyncSession = Depends(get_db_session),
492
493
  ):
493
494
  """
@@ -4,7 +4,8 @@ from pydantic import BaseModel
4
4
  from sqlalchemy.ext.asyncio import AsyncSession
5
5
 
6
6
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
7
- from oauth2fast_fastapi import get_current_verified_user, User
7
+ from oauth2fast_fastapi import User
8
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
8
9
  from files_server_fastapi.files.constants import BASE_DIR
9
10
  from files_server_fastapi.files.dependencies import check_folder_access
10
11
  from files_server_fastapi.files.path_utils import normalize_subpath, build_logical_path
@@ -22,7 +23,7 @@ class FileCreate(BaseModel):
22
23
  @router.post("/create-file", summary="Crear un nuevo archivo en el servidor")
23
24
  async def create_file(
24
25
  req: FileCreate,
25
- current_user: User = Depends(get_current_verified_user),
26
+ current_user: User = Depends(get_active_user),
26
27
  db: AsyncSession = Depends(get_db_session)
27
28
  ):
28
29
  await check_folder_access(area=req.area, subpath=req.subpath, required_access="upload", current_user=current_user, db=db)
@@ -6,7 +6,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
6
6
  from sqlalchemy import select, delete as sql_delete, or_
7
7
 
8
8
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
9
- from oauth2fast_fastapi import get_current_verified_user, User
9
+ from oauth2fast_fastapi import User
10
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
10
11
  from files_server_fastapi.files.constants import BASE_DIR
11
12
  from files_server_fastapi.files.dependencies import check_folder_access
12
13
  from files_server_fastapi.models.rutas_model import Rutas
@@ -23,7 +24,7 @@ class DeleteRequest(BaseModel):
23
24
  @router.delete("/delete", summary="Eliminar un archivo o carpeta del servidor")
24
25
  async def delete_item(
25
26
  req: DeleteRequest,
26
- current_user: User = Depends(get_current_verified_user),
27
+ current_user: User = Depends(get_active_user),
27
28
  db: AsyncSession = Depends(get_db_session),
28
29
  ):
29
30
  """
@@ -3,7 +3,8 @@ from fastapi import HTTPException, Depends
3
3
  from sqlalchemy.ext.asyncio import AsyncSession
4
4
  from sqlmodel import select, or_
5
5
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
6
- from oauth2fast_fastapi import get_current_verified_user, User
6
+ from oauth2fast_fastapi import User
7
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
7
8
  from files_server_fastapi.models.permisos_model import User_Ruta_Access, Permisos, Permiso_rol
8
9
  from files_server_fastapi.models.rutas_model import Rutas
9
10
  from files_server_fastapi.models.users_extend_model import Users_extend
@@ -204,7 +205,7 @@ async def check_folder_access(
204
205
  area: str,
205
206
  subpath: str = "/",
206
207
  required_access: str = "view",
207
- current_user: User = Depends(get_current_verified_user),
208
+ current_user: User = Depends(get_active_user),
208
209
  db: AsyncSession = Depends(get_db_session),
209
210
  ) -> str:
210
211
  """
@@ -6,7 +6,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
6
6
  from sqlalchemy import select
7
7
 
8
8
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
9
- from oauth2fast_fastapi import get_current_verified_user, User
9
+ from oauth2fast_fastapi import User
10
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
10
11
  from files_server_fastapi.files.constants import BASE_DIR
11
12
  from files_server_fastapi.files.dependencies import check_folder_access
12
13
  from files_server_fastapi.files.path_utils import normalize_subpath, build_logical_path
@@ -27,7 +28,7 @@ class FolderCreate(BaseModel):
27
28
  @router.post("/folder", summary="Crear una nueva carpeta en el servidor")
28
29
  async def create_folder(
29
30
  req: FolderCreate,
30
- current_user: User = Depends(get_current_verified_user),
31
+ current_user: User = Depends(get_active_user),
31
32
  db: AsyncSession = Depends(get_db_session)
32
33
  ):
33
34
  logger.debug("folder_router INPUT area=%r subpath=%r folder_name=%r", req.area, req.subpath, req.folder_name)
@@ -5,7 +5,8 @@ from sqlalchemy.ext.asyncio import AsyncSession
5
5
  from sqlmodel import select, or_
6
6
 
7
7
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
8
- from oauth2fast_fastapi import get_current_verified_user, User
8
+ from oauth2fast_fastapi import User
9
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
9
10
 
10
11
  from files_server_fastapi.files.constants import BASE_DIR
11
12
  from files_server_fastapi.files.dependencies import (
@@ -23,7 +24,7 @@ router = APIRouter()
23
24
  async def list_directory(
24
25
  area: str,
25
26
  subpath: str = "/",
26
- current_user: User = Depends(get_current_verified_user),
27
+ current_user: User = Depends(get_active_user),
27
28
  db: AsyncSession = Depends(get_db_session),
28
29
  ):
29
30
  """
@@ -12,7 +12,8 @@ from files_server_fastapi.files.dependencies import (
12
12
  can_edit,
13
13
  can_upload,
14
14
  )
15
- from oauth2fast_fastapi import get_current_verified_user, User
15
+ from oauth2fast_fastapi import User
16
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
16
17
 
17
18
  router = APIRouter()
18
19
 
@@ -27,7 +28,7 @@ async def get_open_url(
27
28
  filename: str,
28
29
  subpath: str = "/",
29
30
  access_type: str = Depends(check_folder_access),
30
- current_user: User = Depends(get_current_verified_user),
31
+ current_user: User = Depends(get_active_user),
31
32
  ):
32
33
  """
33
34
  Devuelve un array **`options`** con las maneras de abrir el archivo según el permiso del usuario.
@@ -12,7 +12,8 @@ from sqlalchemy import select, or_
12
12
  from sqlmodel import select as sm_select
13
13
 
14
14
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
15
- from oauth2fast_fastapi import get_current_verified_user, User
15
+ from oauth2fast_fastapi import User
16
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
16
17
 
17
18
  from files_server_fastapi.files.constants import BASE_DIR
18
19
  from files_server_fastapi.files.dependencies import (
@@ -163,7 +164,7 @@ async def search_files(
163
164
  q: str = Query(..., min_length=1, description="Texto a buscar (nombre de archivo o carpeta)"),
164
165
  area: str = Query(None, description="Área donde buscar (ej: Ventas). Si se omite, busca en todas las áreas accesibles."),
165
166
  limit: int = Query(50, ge=1, le=200, description="Número máximo de resultados"),
166
- current_user: User = Depends(get_current_verified_user),
167
+ current_user: User = Depends(get_active_user),
167
168
  db: AsyncSession = Depends(get_db_session),
168
169
  ):
169
170
  """
@@ -283,7 +284,7 @@ async def search_users(
283
284
  q: str = Query(..., min_length=1, description="Nombre o email del usuario a buscar"),
284
285
  area: str = Query(None, description="Filtrar por área (ej: Ventas). Si se omite, busca en todas las áreas (solo SUPER_ADMIN)."),
285
286
  limit: int = Query(20, ge=1, le=100, description="Número máximo de resultados"),
286
- current_user: User = Depends(get_current_verified_user),
287
+ current_user: User = Depends(get_active_user),
287
288
  db: AsyncSession = Depends(get_db_session),
288
289
  ):
289
290
  """
@@ -2,7 +2,8 @@ import os
2
2
  from fastapi import APIRouter, HTTPException, Depends, UploadFile, File, Form
3
3
  from sqlalchemy.ext.asyncio import AsyncSession
4
4
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
5
- from oauth2fast_fastapi import get_current_verified_user, User
5
+ from oauth2fast_fastapi import User
6
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user
6
7
  from files_server_fastapi.files.constants import BASE_DIR
7
8
  from files_server_fastapi.files.dependencies import check_folder_access
8
9
 
@@ -14,7 +15,7 @@ async def upload_file(
14
15
  area: str = Form(...),
15
16
  subpath: str = Form(default="/"),
16
17
  file: UploadFile = File(...),
17
- current_user: User = Depends(get_current_verified_user),
18
+ current_user: User = Depends(get_active_user),
18
19
  db: AsyncSession = Depends(get_db_session)
19
20
  ):
20
21
  """
@@ -1,5 +1,7 @@
1
1
  from typing import Optional
2
- from datetime import datetime
2
+ from datetime import datetime, timezone
3
+ from sqlalchemy import Column
4
+ from sqlalchemy import DateTime as SADateTime
3
5
  from sqlmodel import Field
4
6
  from oauth2fast_fastapi import AuthModel
5
7
 
@@ -24,7 +26,10 @@ class Users_extend(AuthModel, table=True):
24
26
  # ── Soft Delete ───────────────────────────────────────────────────────────
25
27
  # NULL → usuario activo
26
28
  # NOT NULL → usuario dado de baja (fecha/hora exacta de la baja)
27
- deleted_at: Optional[datetime] = Field(default=None)
29
+ deleted_at: Optional[datetime] = Field(
30
+ default=None,
31
+ sa_column=Column(SADateTime(timezone=True), nullable=True)
32
+ )
28
33
 
29
34
  # ID del usuario (admin/sistemas) que ejecutó la baja. NULL si está activo.
30
35
  deleted_by: Optional[int] = Field(default=None, foreign_key="users.id")
@@ -6,6 +6,7 @@ from pydantic import BaseModel
6
6
 
7
7
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
8
8
  from files_server_fastapi.models.area_model import Area
9
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user, require_superadmin
9
10
 
10
11
  router = APIRouter(prefix="/areas", tags=["Gestión de Áreas"])
11
12
 
@@ -24,7 +25,7 @@ class AreaUpdate(BaseModel):
24
25
  # POST /areas/ — Crear nueva área
25
26
  # ==========================================
26
27
  @router.post("/", response_model=Area, status_code=status.HTTP_201_CREATED, summary="Crear una nueva Área")
27
- async def create_area(area_data: AreaCreate, db: AsyncSession = Depends(get_db_session)):
28
+ async def create_area(area_data: AreaCreate, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
28
29
  """
29
30
  Crea y guarda una nueva área en la base de datos.
30
31
  Solo accesible por superusuarios desde el panel de administración.
@@ -49,7 +50,7 @@ async def create_area(area_data: AreaCreate, db: AsyncSession = Depends(get_db_s
49
50
  # GET /areas/ — Listar todas las áreas
50
51
  # ==========================================
51
52
  @router.get("/", response_model=list[Area], summary="Obtener todas las Áreas")
52
- async def get_areas(db: AsyncSession = Depends(get_db_session)):
53
+ async def get_areas(auth=Depends(get_active_user), db: AsyncSession = Depends(get_db_session)):
53
54
  """
54
55
  Devuelve la lista de todas las áreas registradas.
55
56
  """
@@ -61,7 +62,7 @@ async def get_areas(db: AsyncSession = Depends(get_db_session)):
61
62
  # GET /areas/{area_id} — Obtener área por ID
62
63
  # ==========================================
63
64
  @router.get("/{area_id}", response_model=Area, summary="Obtener un Área por ID")
64
- async def get_area(area_id: int, db: AsyncSession = Depends(get_db_session)):
65
+ async def get_area(area_id: int, auth=Depends(get_active_user), db: AsyncSession = Depends(get_db_session)):
65
66
  """
66
67
  Devuelve los datos de un área específica por su ID.
67
68
  """
@@ -79,7 +80,7 @@ async def get_area(area_id: int, db: AsyncSession = Depends(get_db_session)):
79
80
  # PATCH /areas/{area_id} — Actualizar área
80
81
  # ==========================================
81
82
  @router.patch("/{area_id}", response_model=Area, summary="Actualizar un Área")
82
- async def update_area(area_id: int, area_update: AreaUpdate, db: AsyncSession = Depends(get_db_session)):
83
+ async def update_area(area_id: int, area_update: AreaUpdate, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
83
84
  """
84
85
  Actualiza parcialmente los datos de un área existente.
85
86
  """
@@ -114,7 +115,7 @@ async def update_area(area_id: int, area_update: AreaUpdate, db: AsyncSession =
114
115
  # DELETE /areas/{area_id} — Eliminar área
115
116
  # ==========================================
116
117
  @router.delete("/{area_id}", status_code=status.HTTP_204_NO_CONTENT, summary="Eliminar un Área")
117
- async def delete_area(area_id: int, db: AsyncSession = Depends(get_db_session)):
118
+ async def delete_area(area_id: int, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
118
119
  """
119
120
  Elimina un área por su ID.
120
121
  ADVERTENCIA: Solo eliminar si no hay usuarios asociados a esta área.
@@ -4,24 +4,25 @@ from sqlalchemy import select
4
4
 
5
5
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
6
6
  from files_server_fastapi.models.permisos_model import Permisos, User_Ruta_Access, Permiso_rol
7
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user, require_superadmin
7
8
 
8
9
  router = APIRouter(prefix="/permisos", tags=["Gestión de Permisos"])
9
10
 
10
11
  # --- Catálogo Maestro ---
11
12
  @router.post("/", response_model=Permisos, summary="Crear Permiso Maestro")
12
- async def create_permiso(permiso: Permisos, db: AsyncSession = Depends(get_db_session)):
13
+ async def create_permiso(permiso: Permisos, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
13
14
  db.add(permiso)
14
15
  await db.commit()
15
16
  await db.refresh(permiso)
16
17
  return permiso
17
18
 
18
19
  @router.get("/", response_model=list[Permisos], summary="Ver Permisos Maestros")
19
- async def get_permisos(db: AsyncSession = Depends(get_db_session)):
20
+ async def get_permisos(auth=Depends(get_active_user), db: AsyncSession = Depends(get_db_session)):
20
21
  result = await db.execute(select(Permisos))
21
22
  return result.scalars().all()
22
23
 
23
24
  @router.put("/{permiso_id}", response_model=Permisos, summary="Editar Permiso Maestro")
24
- async def update_permiso(permiso_id: int, permiso_data: Permisos, db: AsyncSession = Depends(get_db_session)):
25
+ async def update_permiso(permiso_id: int, permiso_data: Permisos, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
25
26
  result = await db.execute(select(Permisos).where(Permisos.id == permiso_id))
26
27
  db_permiso = result.scalars().first()
27
28
  if not db_permiso:
@@ -35,7 +36,7 @@ async def update_permiso(permiso_id: int, permiso_data: Permisos, db: AsyncSessi
35
36
  return db_permiso
36
37
 
37
38
  @router.delete("/{permiso_id}", summary="Eliminar Permiso Maestro")
38
- async def delete_permiso(permiso_id: int, db: AsyncSession = Depends(get_db_session)):
39
+ async def delete_permiso(permiso_id: int, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
39
40
  result = await db.execute(select(Permisos).where(Permisos.id == permiso_id))
40
41
  db_permiso = result.scalars().first()
41
42
  if not db_permiso:
@@ -47,26 +48,26 @@ async def delete_permiso(permiso_id: int, db: AsyncSession = Depends(get_db_sess
47
48
 
48
49
  # --- Asignaciones Intermedias ---
49
50
  @router.post("/asignar-acl", response_model=User_Ruta_Access, summary="Asignar o Denegar acceso a ruta (ACL)")
50
- async def assign_acl(acl: User_Ruta_Access, db: AsyncSession = Depends(get_db_session)):
51
+ async def assign_acl(acl: User_Ruta_Access, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
51
52
  db.add(acl)
52
53
  await db.commit()
53
54
  await db.refresh(acl)
54
55
  return acl
55
56
 
56
57
  @router.post("/asignar-rol", response_model=Permiso_rol, summary="Asignar permiso a un Rol")
57
- async def assign_rol(permiso: Permiso_rol, db: AsyncSession = Depends(get_db_session)):
58
+ async def assign_rol(permiso: Permiso_rol, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
58
59
  db.add(permiso)
59
60
  await db.commit()
60
61
  await db.refresh(permiso)
61
62
  return permiso
62
63
 
63
64
  @router.get("/asignaciones-rol", summary="Ver todas las asignaciones de Permisos a Roles")
64
- async def get_asignaciones_rol(db: AsyncSession = Depends(get_db_session)):
65
+ async def get_asignaciones_rol(auth=Depends(get_active_user), db: AsyncSession = Depends(get_db_session)):
65
66
  result = await db.execute(select(Permiso_rol))
66
67
  return result.scalars().all()
67
68
 
68
69
  @router.delete("/asignar-rol/{asignacion_id}", summary="Revocar permiso a un Rol")
69
- async def revoke_rol_permission(asignacion_id: int, db: AsyncSession = Depends(get_db_session)):
70
+ async def revoke_rol_permission(asignacion_id: int, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
70
71
  result = await db.execute(select(Permiso_rol).where(Permiso_rol.id == asignacion_id))
71
72
  db_asig = result.scalars().first()
72
73
  if not db_asig:
@@ -4,7 +4,8 @@ from sqlalchemy import select
4
4
 
5
5
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
6
6
  from files_server_fastapi.models.rol_model import Rol
7
- from files_server_fastapi.dependencies.user_dependencies import require_superadmin
7
+ from files_server_fastapi.models.rol_model import Rol
8
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user, require_superadmin
8
9
 
9
10
  router = APIRouter(prefix="/roles", tags=["Gestión de Roles"])
10
11
 
@@ -30,11 +31,11 @@ async def create_rol(
30
31
 
31
32
  @router.get("/", response_model=list[Rol], summary="Obtener todos los Roles")
32
33
  async def get_roles(
34
+ auth=Depends(get_active_user),
33
35
  db: AsyncSession = Depends(get_db_session)
34
36
  ):
35
37
  """
36
38
  Obtiene la lista de todos los roles.
37
- Público para lectura (necesario para formularios de registro/edición).
38
39
  """
39
40
  result = await db.execute(select(Rol))
40
41
  return result.scalars().all()
@@ -4,17 +4,18 @@ from sqlalchemy import select
4
4
 
5
5
  from pgsqlasync2fast_fastapi.dependencies import get_db_session
6
6
  from files_server_fastapi.models.rutas_model import Rutas
7
+ from files_server_fastapi.dependencies.user_dependencies import get_active_user, require_superadmin
7
8
 
8
9
  router = APIRouter(prefix="/rutas", tags=["Gestión de Rutas"])
9
10
 
10
11
  @router.post("/", response_model=Rutas, summary="Registrar una Ruta")
11
- async def create_ruta(ruta: Rutas, db: AsyncSession = Depends(get_db_session)):
12
+ async def create_ruta(ruta: Rutas, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
12
13
  db.add(ruta)
13
14
  await db.commit()
14
15
  await db.refresh(ruta)
15
16
  return ruta
16
17
 
17
18
  @router.get("/", response_model=list[Rutas], summary="Obtener todas las Rutas")
18
- async def get_rutas(db: AsyncSession = Depends(get_db_session)):
19
+ async def get_rutas(auth=Depends(get_active_user), db: AsyncSession = Depends(get_db_session)):
19
20
  result = await db.execute(select(Rutas))
20
21
  return result.scalars().all()
@@ -84,7 +84,7 @@ async def _validate_area_and_rol(area_id: Optional[int], rol_id: Optional[int],
84
84
  # POST /users-extend/ — Vincular usuario
85
85
  # ==========================================
86
86
  @router.post("/", response_model=Users_extend, status_code=status.HTTP_201_CREATED, summary="Vincular Usuario con Área y Rol")
87
- async def create_user_extend(user_ext: UserExtendCreate, db: AsyncSession = Depends(get_db_session)):
87
+ async def create_user_extend(user_ext: UserExtendCreate, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
88
88
  """
89
89
  Vincula un usuario con un área y rol. Valida que el área y rol existan antes de insertar.
90
90
  """
@@ -110,7 +110,7 @@ async def create_user_extend(user_ext: UserExtendCreate, db: AsyncSession = Depe
110
110
  # GET /users-extend/ — Listar todos
111
111
  # ==========================================
112
112
  @router.get("/", response_model=list[UserExtendResponse], summary="Ver vínculos de usuarios")
113
- async def get_users_extend(db: AsyncSession = Depends(get_db_session)):
113
+ async def get_users_extend(auth=Depends(get_active_user), db: AsyncSession = Depends(get_db_session)):
114
114
  result = await db.execute(select(Users_extend))
115
115
  users = result.scalars().all()
116
116
  # Construimos la respuesta manual inyectando role_id
@@ -126,7 +126,7 @@ async def get_users_extend(db: AsyncSession = Depends(get_db_session)):
126
126
  # GET /users-extend/by-area/{area_id}
127
127
  # ==========================================
128
128
  @router.get("/by-area/{area_id}", response_model=list[UserExtendResponse], summary="Obtener usuarios de un área")
129
- async def get_users_by_area(area_id: int, db: AsyncSession = Depends(get_db_session)):
129
+ async def get_users_by_area(area_id: int, auth=Depends(get_active_user), db: AsyncSession = Depends(get_db_session)):
130
130
  result = await db.execute(select(Users_extend).where(Users_extend.area_id == area_id))
131
131
  users = result.scalars().all()
132
132
  response = []
@@ -141,7 +141,7 @@ async def get_users_by_area(area_id: int, db: AsyncSession = Depends(get_db_sess
141
141
  # GET /users-extend/by-user/{user_id}
142
142
  # ==========================================
143
143
  @router.get("/by-user/{user_id}", summary="Obtener Área y Rol de un Usuario específico")
144
- async def get_user_permissions(user_id: int, db: AsyncSession = Depends(get_db_session)):
144
+ async def get_user_permissions(user_id: int, auth=Depends(get_active_user), db: AsyncSession = Depends(get_db_session)):
145
145
  result = await db.execute(select(Users_extend).where(Users_extend.user_id == user_id))
146
146
  user_ext = result.scalars().first()
147
147
 
@@ -155,7 +155,7 @@ async def get_user_permissions(user_id: int, db: AsyncSession = Depends(get_db_s
155
155
  # PATCH /users-extend/{user_id} — Actualizar
156
156
  # ==========================================
157
157
  @router.patch("/{user_id}", response_model=UserExtendResponse, summary="Actualizar datos de extensión de usuario")
158
- async def update_user_extend(user_id: int, user_ext_update: UserExtendUpdate, db: AsyncSession = Depends(get_db_session)):
158
+ async def update_user_extend(user_id: int, user_ext_update: UserExtendUpdate, auth: tuple = Depends(require_superadmin), db: AsyncSession = Depends(get_db_session)):
159
159
  from sqlmodel import or_
160
160
 
161
161
  # Buscamos por user_id (lo ideal) o por el ID interno del registro (si el frontend se confunde)
@@ -329,16 +329,16 @@ async def deactivate_user(
329
329
  @router.post(
330
330
  "/{user_id}/reactivate",
331
331
  status_code=status.HTTP_200_OK,
332
- summary="Reactivar un usuario dado de baja (solo Sistemas/Superadmin)",
332
+ summary="Reactivar un usuario dado de baja",
333
333
  )
334
334
  async def reactivate_user(
335
335
  user_id: int,
336
- auth: tuple = Depends(require_superadmin),
336
+ auth: tuple = Depends(require_area_admin_or_superadmin),
337
337
  db: AsyncSession = Depends(get_db_session),
338
338
  ):
339
339
  """
340
340
  Reactiva a un usuario previamente dado de baja.
341
- Solo Sistemas/Superadmin puede ejecutar esta acción.
341
+ Admin de Área solo puede reactivar usuarios de su área. Superadmin a todos.
342
342
 
343
343
  El trigger PostgreSQL automáticamente restaura is_verified = TRUE
344
344
  en la tabla users, permitiendo que el usuario vuelva a hacer login.
@@ -354,6 +354,28 @@ async def reactivate_user(
354
354
  detail=f"Usuario con ID {user_id} no encontrado.",
355
355
  )
356
356
 
357
+ current_user, executor_ext, is_superadmin = auth
358
+
359
+ if not is_superadmin:
360
+ # Admin de área: solo puede reactivar a usuarios de su misma área
361
+ if target_ext.area_id != executor_ext.area_id:
362
+ raise HTTPException(
363
+ status_code=status.HTTP_403_FORBIDDEN,
364
+ detail="Solo puedes reactivar a usuarios de tu propia área.",
365
+ )
366
+
367
+ # Admin de área: no puede reactivar a otros admins de área ni superadmins
368
+ from files_server_fastapi.dependencies.user_dependencies import (
369
+ _get_privilege_level,
370
+ PRIVILEGE_AREA_ADMIN,
371
+ )
372
+ target_level = await _get_privilege_level(target_ext.rol_id, db)
373
+ if target_level >= PRIVILEGE_AREA_ADMIN:
374
+ raise HTTPException(
375
+ status_code=status.HTTP_403_FORBIDDEN,
376
+ detail="No puedes reactivar a un Admin de Área o Superadmin. Contacta a Sistemas.",
377
+ )
378
+
357
379
  if target_ext.deleted_at is None:
358
380
  raise HTTPException(
359
381
  status_code=status.HTTP_409_CONFLICT,
@@ -377,19 +399,24 @@ async def reactivate_user(
377
399
  @router.get(
378
400
  "/inactive",
379
401
  status_code=status.HTTP_200_OK,
380
- summary="Listar usuarios dados de baja (Auditoría — solo Sistemas/Superadmin)",
402
+ summary="Listar usuarios dados de baja",
381
403
  )
382
404
  async def list_inactive_users(
383
- auth: tuple = Depends(require_superadmin),
405
+ auth: tuple = Depends(require_area_admin_or_superadmin),
384
406
  db: AsyncSession = Depends(get_db_session),
385
407
  ):
386
408
  """
387
- Retorna todos los usuarios dados de baja con su fecha y responsable de baja.
388
- Solo accesible por Sistemas/Superadmin. Útil para auditoría y trazabilidad.
409
+ Retorna los usuarios dados de baja.
410
+ Admin de Área solo ve los de su área. Superadmin ve todos. Útil para auditoría.
389
411
  """
390
- result = await db.execute(
391
- select(Users_extend).where(Users_extend.deleted_at.is_not(None))
392
- )
412
+ current_user, executor_ext, is_superadmin = auth
413
+
414
+ query = select(Users_extend).where(Users_extend.deleted_at.is_not(None))
415
+
416
+ if not is_superadmin:
417
+ query = query.where(Users_extend.area_id == executor_ext.area_id)
418
+
419
+ result = await db.execute(query)
393
420
  inactive_users = result.scalars().all()
394
421
 
395
422
  return [
@@ -637,7 +637,7 @@ wheels = [
637
637
 
638
638
  [[package]]
639
639
  name = "files-server-fastapi"
640
- version = "0.1.44"
640
+ version = "0.1.45"
641
641
  source = { editable = "." }
642
642
  dependencies = [
643
643
  { name = "oauth2fast-fastapi" },