Noor2623 commited on
Commit
d99646e
·
0 Parent(s):

Initial commit for Smart Query Routing System

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +4 -0
  2. .gitignore +0 -0
  3. Dockerfile +0 -0
  4. alembic.ini +0 -0
  5. app/__init__.py +0 -0
  6. app/__pycache__/__init__.cpython-313.pyc +0 -0
  7. app/__pycache__/config.cpython-313.pyc +0 -0
  8. app/__pycache__/dependencies.cpython-313.pyc +0 -0
  9. app/__pycache__/main.cpython-313.pyc +0 -0
  10. app/config.py +47 -0
  11. app/core/__init__.py +0 -0
  12. app/core/__pycache__/__init__.cpython-313.pyc +0 -0
  13. app/core/__pycache__/database.cpython-313.pyc +0 -0
  14. app/core/__pycache__/exceptions.cpython-313.pyc +0 -0
  15. app/core/__pycache__/logging.cpython-313.pyc +0 -0
  16. app/core/__pycache__/permissions.cpython-313.pyc +0 -0
  17. app/core/__pycache__/security.cpython-313.pyc +0 -0
  18. app/core/database.py +31 -0
  19. app/core/exceptions.py +0 -0
  20. app/core/logging.py +0 -0
  21. app/core/permissions.py +0 -0
  22. app/core/security.py +41 -0
  23. app/dependencies.py +60 -0
  24. app/main.py +35 -0
  25. app/models/__init__.py +4 -0
  26. app/models/__pycache__/__init__.cpython-313.pyc +0 -0
  27. app/models/__pycache__/announcement.cpython-313.pyc +0 -0
  28. app/models/__pycache__/audit_log.cpython-313.pyc +0 -0
  29. app/models/__pycache__/department.cpython-313.pyc +0 -0
  30. app/models/__pycache__/escalation.cpython-313.pyc +0 -0
  31. app/models/__pycache__/instructor_leave.cpython-313.pyc +0 -0
  32. app/models/__pycache__/notification.cpython-313.pyc +0 -0
  33. app/models/__pycache__/query.cpython-313.pyc +0 -0
  34. app/models/__pycache__/query_message.cpython-313.pyc +0 -0
  35. app/models/__pycache__/routing_rule.cpython-313.pyc +0 -0
  36. app/models/__pycache__/user.cpython-313.pyc +0 -0
  37. app/models/announcement.py +0 -0
  38. app/models/audit_log.py +0 -0
  39. app/models/department.py +16 -0
  40. app/models/escalation.py +0 -0
  41. app/models/instructor_leave.py +0 -0
  42. app/models/notification.py +0 -0
  43. app/models/query.py +0 -0
  44. app/models/query_message.py +0 -0
  45. app/models/routing_rule.py +0 -0
  46. app/models/user.py +30 -0
  47. app/routes/__pycache__/admin.cpython-313.pyc +0 -0
  48. app/routes/__pycache__/auth.cpython-313.pyc +0 -0
  49. app/routes/__pycache__/departments.cpython-313.pyc +0 -0
  50. app/routes/admin.py +135 -0
.env ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ APP_NAME=QueryRoute Dashboard
2
+ ENVIRONMENT=development
3
+ DEBUG=true
4
+ DATABASE_URL=postgresql://neondb_owner:npg_ltTPcqu1Q6oS@ep-damp-glade-ab350rcx.eu-west-2.aws.neon.tech/neondb?sslmode=require&channel_binding=require
.gitignore ADDED
File without changes
Dockerfile ADDED
File without changes
alembic.ini ADDED
File without changes
app/__init__.py ADDED
File without changes
app/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (167 Bytes). View file
 
app/__pycache__/config.cpython-313.pyc ADDED
Binary file (3.04 kB). View file
 
app/__pycache__/dependencies.cpython-313.pyc ADDED
Binary file (2.33 kB). View file
 
app/__pycache__/main.cpython-313.pyc ADDED
Binary file (1.88 kB). View file
 
app/config.py ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from dataclasses import dataclass
2
+ from pathlib import Path
3
+ import os
4
+
5
+
6
+ def _load_env_file() -> None:
7
+ env_path = Path(__file__).resolve().parents[1] / ".env"
8
+ if not env_path.exists():
9
+ return
10
+
11
+ for raw_line in env_path.read_text(encoding="utf-8").splitlines():
12
+ line = raw_line.strip()
13
+ if not line or line.startswith("#") or "=" not in line:
14
+ continue
15
+
16
+ key, value = line.split("=", 1)
17
+ key = key.strip()
18
+ value = value.strip().strip('"').strip("'")
19
+
20
+ if key and key not in os.environ:
21
+ os.environ[key] = value
22
+
23
+
24
+ _load_env_file()
25
+
26
+
27
+ def _as_bool(value: str | None, default: bool = False) -> bool:
28
+ if value is None:
29
+ return default
30
+ return value.strip().lower() in {"1", "true", "yes", "on"}
31
+
32
+
33
+ @dataclass(frozen=True)
34
+ class Settings:
35
+ app_name: str = os.getenv("APP_NAME", "QueryRoute Dashboard")
36
+ environment: str = os.getenv("ENVIRONMENT", "development")
37
+ debug: bool = _as_bool(os.getenv("DEBUG"), False)
38
+ database_url: str = os.getenv("DATABASE_URL", "")
39
+ database_url_direct: str = os.getenv("DATABASE_URL_DIRECT", "")
40
+ jwt_secret_key: str = os.getenv("JWT_SECRET_KEY", "change-me-in-production")
41
+ jwt_algorithm: str = os.getenv("JWT_ALGORITHM", "HS256")
42
+ access_token_expire_minutes: int = int(
43
+ os.getenv("ACCESS_TOKEN_EXPIRE_MINUTES", "60")
44
+ )
45
+
46
+
47
+ settings = Settings()
app/core/__init__.py ADDED
File without changes
app/core/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (172 Bytes). View file
 
app/core/__pycache__/database.cpython-313.pyc ADDED
Binary file (1.02 kB). View file
 
app/core/__pycache__/exceptions.cpython-313.pyc ADDED
Binary file (119 Bytes). View file
 
app/core/__pycache__/logging.cpython-313.pyc ADDED
Binary file (116 Bytes). View file
 
app/core/__pycache__/permissions.cpython-313.pyc ADDED
Binary file (120 Bytes). View file
 
app/core/__pycache__/security.cpython-313.pyc ADDED
Binary file (2.09 kB). View file
 
app/core/database.py ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Generator
2
+
3
+ from sqlalchemy import create_engine
4
+ from sqlalchemy.orm import declarative_base, sessionmaker
5
+
6
+ from app.config import settings
7
+
8
+
9
+ if not settings.database_url:
10
+ raise RuntimeError("DATABASE_URL is not set")
11
+
12
+ engine = create_engine(
13
+ settings.database_url,
14
+ pool_pre_ping=True,
15
+ )
16
+
17
+ SessionLocal = sessionmaker(
18
+ autocommit=False,
19
+ autoflush=False,
20
+ bind=engine,
21
+ )
22
+
23
+ Base = declarative_base()
24
+
25
+
26
+ def get_db() -> Generator:
27
+ db = SessionLocal()
28
+ try:
29
+ yield db
30
+ finally:
31
+ db.close()
app/core/exceptions.py ADDED
File without changes
app/core/logging.py ADDED
File without changes
app/core/permissions.py ADDED
File without changes
app/core/security.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from datetime import UTC, datetime, timedelta
4
+
5
+ import bcrypt
6
+ import jwt
7
+
8
+ from app.config import settings
9
+
10
+
11
+ def hash_password(password: str) -> str:
12
+ salt = bcrypt.gensalt()
13
+ return bcrypt.hashpw(password.encode("utf-8"), salt).decode("utf-8")
14
+
15
+
16
+ def verify_password(plain_password: str, hashed_password: str) -> bool:
17
+ try:
18
+ return bcrypt.checkpw(
19
+ plain_password.encode("utf-8"),
20
+ hashed_password.encode("utf-8"),
21
+ )
22
+ except ValueError:
23
+ return False
24
+
25
+
26
+ def create_access_token(subject: str, expires_delta: timedelta | None = None) -> str:
27
+ expire = datetime.now(UTC) + (
28
+ expires_delta
29
+ if expires_delta is not None
30
+ else timedelta(minutes=settings.access_token_expire_minutes)
31
+ )
32
+ payload = {"sub": subject, "exp": expire}
33
+ return jwt.encode(payload, settings.jwt_secret_key, algorithm=settings.jwt_algorithm)
34
+
35
+
36
+ def decode_access_token(token: str) -> dict:
37
+ return jwt.decode(
38
+ token,
39
+ settings.jwt_secret_key,
40
+ algorithms=[settings.jwt_algorithm],
41
+ )
app/dependencies.py ADDED
@@ -0,0 +1,60 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Generator
2
+
3
+ from fastapi import Depends, HTTPException, status
4
+ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
5
+ from sqlalchemy.orm import Session
6
+ import jwt
7
+
8
+ from app.core.database import SessionLocal
9
+ from app.core.security import decode_access_token
10
+ from app.models.user import User
11
+
12
+
13
+ bearer_scheme = HTTPBearer(auto_error=False)
14
+
15
+
16
+ def get_db() -> Generator[Session, None, None]:
17
+ db = SessionLocal()
18
+ try:
19
+ yield db
20
+ finally:
21
+ db.close()
22
+
23
+
24
+ def get_current_user(
25
+ credentials: HTTPAuthorizationCredentials | None = Depends(bearer_scheme),
26
+ db: Session = Depends(get_db),
27
+ ) -> User:
28
+ if credentials is None:
29
+ raise HTTPException(
30
+ status_code=status.HTTP_401_UNAUTHORIZED,
31
+ detail="Not authenticated",
32
+ )
33
+
34
+ try:
35
+ payload = decode_access_token(credentials.credentials)
36
+ user_id = int(payload["sub"])
37
+ except (KeyError, ValueError, TypeError, jwt.PyJWTError):
38
+ raise HTTPException(
39
+ status_code=status.HTTP_401_UNAUTHORIZED,
40
+ detail="Invalid token",
41
+ ) from None
42
+
43
+ user = db.get(User, user_id)
44
+ if user is None:
45
+ raise HTTPException(
46
+ status_code=status.HTTP_401_UNAUTHORIZED,
47
+ detail="User not found",
48
+ )
49
+ return user
50
+
51
+
52
+ def get_current_admin_user(
53
+ current_user: User = Depends(get_current_user),
54
+ ) -> User:
55
+ if not current_user.is_admin:
56
+ raise HTTPException(
57
+ status_code=status.HTTP_403_FORBIDDEN,
58
+ detail="Admin access required",
59
+ )
60
+ return current_user
app/main.py ADDED
@@ -0,0 +1,35 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI
2
+ from sqlalchemy import text
3
+
4
+
5
+ from app.config import settings
6
+ from app.core.database import engine
7
+ from app.models import User # noqa: F401
8
+ from app.core.database import Base
9
+ from app.routes.auth import router as auth_router
10
+ from app.routes.admin import router as admin_router
11
+ from app.routes.departments import router as departments_router
12
+
13
+
14
+ app = FastAPI(title=settings.app_name)
15
+
16
+ app.include_router(auth_router)
17
+ app.include_router(admin_router)
18
+ app.include_router(departments_router)
19
+
20
+
21
+ @app.on_event("startup")
22
+ def startup() -> None:
23
+ Base.metadata.create_all(bind=engine)
24
+
25
+
26
+ @app.get("/")
27
+ def read_root() -> dict[str, str]:
28
+ return {"status": "ok", "app": settings.app_name}
29
+
30
+
31
+ @app.get("/health/db")
32
+ def health_db() -> dict[str, str]:
33
+ with engine.connect() as connection:
34
+ connection.execute(text("SELECT 1"))
35
+ return {"status": "ok", "database": "connected"}
app/models/__init__.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ from app.models.user import User
2
+ from app.models.department import Department
3
+
4
+ __all__ = ["User", "Department"]
app/models/__pycache__/__init__.cpython-313.pyc ADDED
Binary file (260 Bytes). View file
 
app/models/__pycache__/announcement.cpython-313.pyc ADDED
Binary file (123 Bytes). View file
 
app/models/__pycache__/audit_log.cpython-313.pyc ADDED
Binary file (120 Bytes). View file
 
app/models/__pycache__/department.cpython-313.pyc ADDED
Binary file (1.04 kB). View file
 
app/models/__pycache__/escalation.cpython-313.pyc ADDED
Binary file (121 Bytes). View file
 
app/models/__pycache__/instructor_leave.cpython-313.pyc ADDED
Binary file (127 Bytes). View file
 
app/models/__pycache__/notification.cpython-313.pyc ADDED
Binary file (123 Bytes). View file
 
app/models/__pycache__/query.cpython-313.pyc ADDED
Binary file (116 Bytes). View file
 
app/models/__pycache__/query_message.cpython-313.pyc ADDED
Binary file (124 Bytes). View file
 
app/models/__pycache__/routing_rule.cpython-313.pyc ADDED
Binary file (123 Bytes). View file
 
app/models/__pycache__/user.cpython-313.pyc ADDED
Binary file (2.16 kB). View file
 
app/models/announcement.py ADDED
File without changes
app/models/audit_log.py ADDED
File without changes
app/models/department.py ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlalchemy import Boolean, Column, Integer, String, Text
2
+ from sqlalchemy.orm import relationship
3
+
4
+ from app.core.database import Base
5
+
6
+
7
+ class Department(Base):
8
+ __tablename__ = "departments"
9
+
10
+ id = Column(Integer, primary_key=True, index=True)
11
+ name = Column(String(100), unique=True, nullable=False)
12
+ code = Column(String(20), unique=True, nullable=False)
13
+ description = Column(Text, nullable=True)
14
+ keywords = Column(Text, nullable=True)
15
+ is_active = Column(Boolean, default=True, nullable=False)
16
+ users = relationship("User", back_populates="department")
app/models/escalation.py ADDED
File without changes
app/models/instructor_leave.py ADDED
File without changes
app/models/notification.py ADDED
File without changes
app/models/query.py ADDED
File without changes
app/models/query_message.py ADDED
File without changes
app/models/routing_rule.py ADDED
File without changes
app/models/user.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import UTC, datetime
2
+ from sqlalchemy import Boolean, DateTime, ForeignKey, Integer, String
3
+ from sqlalchemy.orm import Mapped, mapped_column, relationship
4
+ from app.core.database import Base
5
+
6
+
7
+ class User(Base):
8
+ __tablename__ = "users"
9
+
10
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
11
+ email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
12
+ full_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
13
+ hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)
14
+ role: Mapped[str] = mapped_column(String(255), nullable=False)
15
+ is_admin: Mapped[bool] = mapped_column(Boolean, default=False, nullable=False)
16
+ department_id: Mapped[int | None] = mapped_column(
17
+ ForeignKey("departments.id", ondelete="SET NULL"),
18
+ nullable=True,
19
+ )
20
+ is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)
21
+ created_at: Mapped[datetime] = mapped_column(
22
+ DateTime, default=lambda: datetime.now(UTC), nullable=False
23
+ )
24
+ department = relationship("Department", back_populates="users")
25
+
26
+ @property
27
+ def department_name(self) -> str | None:
28
+ if self.department is None:
29
+ return None
30
+ return self.department.name
app/routes/__pycache__/admin.cpython-313.pyc ADDED
Binary file (5.73 kB). View file
 
app/routes/__pycache__/auth.cpython-313.pyc ADDED
Binary file (2.19 kB). View file
 
app/routes/__pycache__/departments.cpython-313.pyc ADDED
Binary file (5.21 kB). View file
 
app/routes/admin.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException, status
2
+ from sqlalchemy.orm import Session
3
+
4
+ from app.dependencies import get_current_admin_user, get_db
5
+ from app.models.user import User
6
+ from app.schemas.admin import AdminDepartmentUpdate, AdminUserUpdate
7
+ from app.schemas.department import DepartmentCreate, DepartmentResponse
8
+ from app.schemas.user import UserRead
9
+ from app.services.admin_service import (
10
+ create_department,
11
+ deactivate_department,
12
+ deactivate_user,
13
+ get_department,
14
+ get_user,
15
+ list_departments,
16
+ list_users,
17
+ update_department,
18
+ update_user,
19
+ )
20
+
21
+
22
+ router = APIRouter(prefix="/admin", tags=["admin"])
23
+
24
+
25
+ def _raise_not_found(entity: str) -> None:
26
+ raise HTTPException(
27
+ status_code=status.HTTP_404_NOT_FOUND,
28
+ detail=f"{entity} not found",
29
+ )
30
+
31
+
32
+ @router.get("/departments", response_model=list[DepartmentResponse])
33
+ def admin_list_departments(
34
+ db: Session = Depends(get_db),
35
+ current_admin: User = Depends(get_current_admin_user),
36
+ ):
37
+ _ = current_admin
38
+ return list_departments(db)
39
+
40
+
41
+ @router.post("/departments", response_model=DepartmentResponse, status_code=status.HTTP_201_CREATED)
42
+ def admin_create_department(
43
+ payload: DepartmentCreate,
44
+ db: Session = Depends(get_db),
45
+ current_admin: User = Depends(get_current_admin_user),
46
+ ):
47
+ _ = current_admin
48
+ try:
49
+ return create_department(db, **payload.model_dump())
50
+ except ValueError as exc:
51
+ raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
52
+
53
+
54
+ @router.put("/departments/{department_id}", response_model=DepartmentResponse)
55
+ def admin_update_department(
56
+ department_id: int,
57
+ payload: AdminDepartmentUpdate,
58
+ db: Session = Depends(get_db),
59
+ current_admin: User = Depends(get_current_admin_user),
60
+ ):
61
+ _ = current_admin
62
+ try:
63
+ department = update_department(db, department_id=department_id, payload=payload)
64
+ except ValueError as exc:
65
+ message = str(exc)
66
+ status_code = status.HTTP_404_NOT_FOUND if message == "Department not found" else status.HTTP_400_BAD_REQUEST
67
+ raise HTTPException(status_code=status_code, detail=message) from exc
68
+ return department
69
+
70
+
71
+ @router.delete("/departments/{department_id}", response_model=DepartmentResponse)
72
+ def admin_deactivate_department(
73
+ department_id: int,
74
+ db: Session = Depends(get_db),
75
+ current_admin: User = Depends(get_current_admin_user),
76
+ ):
77
+ _ = current_admin
78
+ try:
79
+ department = deactivate_department(db, department_id=department_id)
80
+ except ValueError as exc:
81
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
82
+ return department
83
+
84
+
85
+ @router.get("/users", response_model=list[UserRead])
86
+ def admin_list_users(
87
+ db: Session = Depends(get_db),
88
+ current_admin: User = Depends(get_current_admin_user),
89
+ ):
90
+ _ = current_admin
91
+ return list_users(db)
92
+
93
+
94
+ @router.get("/users/{user_id}", response_model=UserRead)
95
+ def admin_get_user(
96
+ user_id: int,
97
+ db: Session = Depends(get_db),
98
+ current_admin: User = Depends(get_current_admin_user),
99
+ ):
100
+ _ = current_admin
101
+ user = get_user(db, user_id=user_id)
102
+ if user is None:
103
+ _raise_not_found("User")
104
+ return user
105
+
106
+
107
+ @router.put("/users/{user_id}", response_model=UserRead)
108
+ def admin_update_user(
109
+ user_id: int,
110
+ payload: AdminUserUpdate,
111
+ db: Session = Depends(get_db),
112
+ current_admin: User = Depends(get_current_admin_user),
113
+ ):
114
+ _ = current_admin
115
+ try:
116
+ user = update_user(db, user_id=user_id, payload=payload)
117
+ except ValueError as exc:
118
+ message = str(exc)
119
+ status_code = status.HTTP_404_NOT_FOUND if message == "User not found" else status.HTTP_400_BAD_REQUEST
120
+ raise HTTPException(status_code=status_code, detail=message) from exc
121
+ return user
122
+
123
+
124
+ @router.delete("/users/{user_id}", response_model=UserRead)
125
+ def admin_deactivate_user(
126
+ user_id: int,
127
+ db: Session = Depends(get_db),
128
+ current_admin: User = Depends(get_current_admin_user),
129
+ ):
130
+ _ = current_admin
131
+ try:
132
+ user = deactivate_user(db, user_id=user_id)
133
+ except ValueError as exc:
134
+ raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=str(exc)) from exc
135
+ return user