From 01863c1df0a93f67c233a7c3c85df1f61eeedcc3 Mon Sep 17 00:00:00 2001 From: mahmamdouh Date: Sun, 25 Jan 2026 21:02:45 +0100 Subject: [PATCH] feature1 --- backend/main.py | 3 +- backend/models.py | 20 +++++ backend/routers/apps.py | 12 ++- backend/routers/requests.py | 98 ++++++++++++++++++++++++ backend/schemas.py | 18 +++++ backend/services/email.py | 22 ++++++ deploy.sh | 2 +- frontend/index.html | 26 +++++++ frontend/register.html | 148 ++++++++++++++++++++++++++++++++++++ frontend/static/js/app.js | 57 +++++++++++++- 10 files changed, 399 insertions(+), 7 deletions(-) create mode 100644 backend/routers/requests.py create mode 100644 frontend/register.html diff --git a/backend/main.py b/backend/main.py index 2165611..cf44e08 100644 --- a/backend/main.py +++ b/backend/main.py @@ -2,7 +2,7 @@ from fastapi import FastAPI, Depends from fastapi.staticfiles import StaticFiles from fastapi.middleware.cors import CORSMiddleware from .database import engine, Base, SessionLocal -from .routers import auth, users, apps, sso +from .routers import auth, users, apps, sso, requests from . import models, auth_utils from sqlalchemy.orm import Session @@ -25,6 +25,7 @@ app.include_router(auth.router) app.include_router(users.router) app.include_router(apps.router) app.include_router(sso.router) +app.include_router(requests.router) import os diff --git a/backend/models.py b/backend/models.py index b501ccf..d23fc48 100644 --- a/backend/models.py +++ b/backend/models.py @@ -38,3 +38,23 @@ class UserApplication(Base): user = relationship("User", back_populates="applications") application = relationship("Application", back_populates="users") + +# Association table for AccessRequest and Application +class RequestApplication(Base): + __tablename__ = "request_applications" + + id = Column(Integer, primary_key=True, index=True) + request_id = Column(Integer, ForeignKey("access_requests.id")) + application_id = Column(Integer, ForeignKey("applications.id")) + +class AccessRequest(Base): + __tablename__ = "access_requests" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String, index=True) + email = Column(String, index=True) + hashed_password = Column(String) + status = Column(String, default="pending") # pending, approved, rejected + created_at = Column(DateTime, default=datetime.utcnow) + + requested_apps = relationship("Application", secondary="request_applications") diff --git a/backend/routers/apps.py b/backend/routers/apps.py index f0dadb4..e5078cb 100644 --- a/backend/routers/apps.py +++ b/backend/routers/apps.py @@ -6,11 +6,10 @@ from .. import database, models, schemas, auth_utils router = APIRouter( prefix="/apps", - tags=["Applications"], - dependencies=[Depends(auth_utils.get_current_admin_user)] + tags=["Applications"] ) -@router.post("/", response_model=schemas.ApplicationOut) +@router.post("/", response_model=schemas.ApplicationOut, dependencies=[Depends(auth_utils.get_current_admin_user)]) async def create_application(app: schemas.ApplicationCreate, db: Session = Depends(database.get_db)): db_app = db.query(models.Application).filter(models.Application.name == app.name).first() if db_app: @@ -27,7 +26,12 @@ async def create_application(app: schemas.ApplicationCreate, db: Session = Depen db.refresh(db_app) return db_app -@router.get("/", response_model=List[schemas.ApplicationOut]) +@router.get("/", response_model=List[schemas.ApplicationOut], dependencies=[Depends(auth_utils.get_current_admin_user)]) async def read_applications(skip: int = 0, limit: int = 100, db: Session = Depends(database.get_db)): apps = db.query(models.Application).offset(skip).limit(limit).all() return apps + +@router.get("/public", response_model=List[schemas.ApplicationOut]) +async def read_public_applications(db: Session = Depends(database.get_db)): + apps = db.query(models.Application).all() + return apps diff --git a/backend/routers/requests.py b/backend/routers/requests.py new file mode 100644 index 0000000..02bb38d --- /dev/null +++ b/backend/routers/requests.py @@ -0,0 +1,98 @@ +from fastapi import APIRouter, Depends, HTTPException, status, BackgroundTasks +from sqlalchemy.orm import Session +from typing import List +from .. import database, models, schemas, auth_utils +from ..services import email + +router = APIRouter( + prefix="/requests", + tags=["Access Requests"] +) + +@router.post("/", response_model=schemas.AccessRequestOut) +async def create_request(request: schemas.AccessRequestCreate, background_tasks: BackgroundTasks, db: Session = Depends(database.get_db)): + # Check if user already exists + existing_user = db.query(models.User).filter(models.User.username == request.username).first() + if existing_user: + raise HTTPException(status_code=400, detail="Username already taken") + + # Check if request already exists + existing_req = db.query(models.AccessRequest).filter(models.AccessRequest.username == request.username, models.AccessRequest.status == "pending").first() + if existing_req: + raise HTTPException(status_code=400, detail="Pending request already exists for this username") + + hashed_password = auth_utils.get_password_hash(request.password) + + db_request = models.AccessRequest( + username=request.username, + email=request.email, + hashed_password=hashed_password, + status="pending" + ) + + # Add requested apps + for app_id in request.app_ids: + app = db.query(models.Application).filter(models.Application.id == app_id).first() + if app: + db_request.requested_apps.append(app) + + db.add(db_request) + db.commit() + db.refresh(db_request) + + # Send email to user (Received) + background_tasks.add_task(email.send_request_received_email, request.email, request.username) + # Send email to admin (New Request) + background_tasks.add_task(email.send_admin_notification_email, request.username) + + return db_request + +@router.get("/", response_model=List[schemas.AccessRequestOut], dependencies=[Depends(auth_utils.get_current_admin_user)]) +async def read_requests(skip: int = 0, limit: int = 100, db: Session = Depends(database.get_db)): + requests = db.query(models.AccessRequest).filter(models.AccessRequest.status == "pending").offset(skip).limit(limit).all() + return requests + +@router.post("/{request_id}/approve", dependencies=[Depends(auth_utils.get_current_admin_user)]) +async def approve_request(request_id: int, background_tasks: BackgroundTasks, db: Session = Depends(database.get_db)): + req = db.query(models.AccessRequest).filter(models.AccessRequest.id == request_id).first() + if not req: + raise HTTPException(status_code=404, detail="Request not found") + + if req.status != "pending": + raise HTTPException(status_code=400, detail="Request already processed") + + # Create User + new_user = models.User( + username=req.username, + email=req.email, + hashed_password=req.hashed_password, + is_active=True, + is_admin=False + ) + db.add(new_user) + db.commit() + db.refresh(new_user) + + # Assign Apps + for app in req.requested_apps: + assignment = models.UserApplication(user_id=new_user.id, application_id=app.id) + db.add(assignment) + + req.status = "approved" + db.commit() + + # Send email to user (Approved) + background_tasks.add_task(email.send_welcome_email, req.email, req.username, "********") # Don't send password again, they know it + + return {"message": "Request approved and user created"} + +@router.post("/{request_id}/reject", dependencies=[Depends(auth_utils.get_current_admin_user)]) +async def reject_request(request_id: int, db: Session = Depends(database.get_db)): + req = db.query(models.AccessRequest).filter(models.AccessRequest.id == request_id).first() + if not req: + raise HTTPException(status_code=404, detail="Request not found") + + req.status = "rejected" + db.commit() + + return {"message": "Request rejected"} diff --git a/backend/schemas.py b/backend/schemas.py index 20aedf6..80f8b8f 100644 --- a/backend/schemas.py +++ b/backend/schemas.py @@ -69,3 +69,21 @@ class SSOVerifyResponse(BaseModel): authorized: bool message: str user: Optional[UserOut] = None + +# Access Request Schemas +class AccessRequestCreate(BaseModel): + username: str + email: EmailStr + password: str + app_ids: List[int] = [] + +class AccessRequestOut(BaseModel): + id: int + username: str + email: EmailStr + status: str + created_at: datetime + requested_apps: List[ApplicationOut] = [] + + class Config: + orm_mode = True diff --git a/backend/services/email.py b/backend/services/email.py index 264c06c..45a1f09 100644 --- a/backend/services/email.py +++ b/backend/services/email.py @@ -43,3 +43,25 @@ async def send_welcome_email(to_email: str, username: str, password: str): ASF Team """ await send_email(to_email, subject, body) + +async def send_request_received_email(to_email: str, username: str): + subject = "Access Request Received" + body = f""" + Hello {username}, + + We have received your request for access to the ASF SSO platform. + An administrator will review your request shortly. + + Regards, + ASF Team + """ + await send_email(to_email, subject, body) + +async def send_admin_notification_email(username: str): + to_email = "admin@nabd-co.com" # Default admin email + subject = "New Access Request" + body = f""" + A new access request has been submitted by {username}. + Please log in to the admin portal to review it. + """ + await send_email(to_email, subject, body) diff --git a/deploy.sh b/deploy.sh index 4dbc3a3..bd92fc0 100644 --- a/deploy.sh +++ b/deploy.sh @@ -2,7 +2,7 @@ # Build and start the container # Stop and remove old container and volume to ensure clean state -docker-compose down -v +docker-compose down docker rm -f sso_service || true # Build and start the container diff --git a/frontend/index.html b/frontend/index.html index 214e9ff..1f171b6 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -38,6 +38,7 @@
+
@@ -88,6 +89,31 @@ + + + diff --git a/frontend/register.html b/frontend/register.html new file mode 100644 index 0000000..25bed67 --- /dev/null +++ b/frontend/register.html @@ -0,0 +1,148 @@ + + + + + + + ASF SSO - Request Access + + + + + +
+
+ +

Request Access

+ +
+ + + + +

Select Applications

+
+

Loading applications...

+
+ + +
+ + +

+ Back to Login +

+
+
+ + + + + \ No newline at end of file diff --git a/frontend/static/js/app.js b/frontend/static/js/app.js index 16fdd50..f5fac73 100644 --- a/frontend/static/js/app.js +++ b/frontend/static/js/app.js @@ -14,6 +14,8 @@ const usersSection = document.getElementById("users-section"); const appsSection = document.getElementById("apps-section"); const usersTableBody = document.querySelector("#users-table tbody"); const appsTableBody = document.querySelector("#apps-table tbody"); +const requestsSection = document.getElementById("requests-section"); +const requestsTableBody = document.querySelector("#requests-table tbody"); // Init function init() { @@ -40,11 +42,18 @@ function showSection(section) { if (section === 'users') { usersSection.classList.remove("hidden"); appsSection.classList.add("hidden"); + requestsSection.classList.add("hidden"); loadUsers(); - } else { + } else if (section === 'apps') { usersSection.classList.add("hidden"); appsSection.classList.remove("hidden"); + requestsSection.classList.add("hidden"); loadApps(); + } else { + usersSection.classList.add("hidden"); + appsSection.classList.add("hidden"); + requestsSection.classList.remove("hidden"); + loadRequests(); } } @@ -156,6 +165,52 @@ async function loadApps() { `).join(""); } +// Requests +async function loadRequests() { + const res = await authFetch(`${API_URL}/requests/`); + const requests = await res.json(); + requestsTableBody.innerHTML = requests.map(req => { + const appsList = req.requested_apps.map(app => + `${app.name}` + ).join(" "); + + return ` + + ${req.id} + ${req.username} + ${req.email} + ${appsList} + ${req.status} + + + + + + `}).join(""); +} + +window.approveRequest = async (id) => { + if (!confirm("Approve this request?")) return; + try { + const res = await authFetch(`${API_URL}/requests/${id}/approve`, { method: "POST" }); + if (!res.ok) throw new Error("Failed to approve"); + loadRequests(); + } catch (err) { + alert(err.message); + } +}; + +window.rejectRequest = async (id) => { + if (!confirm("Reject this request?")) return; + try { + const res = await authFetch(`${API_URL}/requests/${id}/reject`, { method: "POST" }); + if (!res.ok) throw new Error("Failed to reject"); + loadRequests(); + } catch (err) { + alert(err.message); + } +}; + // Modals function openModal(id) { document.getElementById(id).classList.remove("hidden");