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 @@
| ID | +Username | +Requested Apps | +Status | +Actions | +
|---|