diff --git a/api/auth.py b/api/auth.py index e5c3450..3d02883 100644 --- a/api/auth.py +++ b/api/auth.py @@ -4,7 +4,7 @@ from jose import JWTError, jwt from passlib.context import CryptContext from pydantic import BaseModel, EmailStr -SECRET_KEY = "your-secret-key-change-in-production" +SECRET_KEY = "your-secret-key-change-in-production" ALGORITHM = "HS256" ACCESS_TOKEN_EXPIRE_MINUTES = 30 @@ -30,6 +30,10 @@ class UserResponse(BaseModel): id: int email: str full_name: Optional[str] = None + role: str = "user" # ← nouveau + +class RoleUpdate(BaseModel): + role: str def verify_password(plain_password: str, hashed_password: str) -> bool: return pwd_context.verify(plain_password, hashed_password) @@ -44,8 +48,7 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None): else: expire = datetime.now(timezone.utc) + timedelta(minutes=15) to_encode.update({"exp": expire}) - encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) - return encoded_jwt + return jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) def decode_token(token: str) -> Optional[str]: try: diff --git a/api/main.py b/api/main.py index 728f29e..75c7a8e 100644 --- a/api/main.py +++ b/api/main.py @@ -1,10 +1,12 @@ from fastapi import FastAPI, Depends, HTTPException, status from fastapi.middleware.cors import CORSMiddleware +from sqlalchemy import text from datetime import timedelta from database.database import SessionLocal from database.models import Document, User from api.auth import ( - UserLogin, UserCreate, Token, UserResponse, get_password_hash, verify_password, + UserLogin, UserCreate, Token, UserResponse, RoleUpdate, + get_password_hash, verify_password, create_access_token, decode_token, ACCESS_TOKEN_EXPIRE_MINUTES ) @@ -18,113 +20,76 @@ app.add_middleware( allow_headers=["*"], ) +# ── Fonction vérification admin ────────────────────────────── +def require_admin(token: str): + email = decode_token(token) + if not email: + raise HTTPException(status_code=401, detail="Invalid token") + db = SessionLocal() + user = db.query(User).filter(User.email == email).first() + db.close() + if not user or user.role != "admin": + raise HTTPException(status_code=403, detail="Accès refusé") + return user + +# ── Endpoints existants ────────────────────────────────────── + @app.get("/") def root(): return {"message": "RegWatch API running"} -@app.post("/test-login") -def test_login(credentials: UserLogin): - return {"received": credentials.dict()} - @app.post("/signup", response_model=UserResponse) def signup(user: UserCreate): db = SessionLocal() - existing_user = db.query(User).filter(User.email == user.email).first() if existing_user: db.close() - raise HTTPException( - status_code=status.HTTP_400_BAD_REQUEST, - detail="Email already registered" - ) - + raise HTTPException(status_code=400, detail="Email already registered") hashed_password = get_password_hash(user.password) - db_user = User( - email=user.email, - password=hashed_password, - full_name=user.full_name - ) + db_user = User(email=user.email, password=hashed_password, full_name=user.full_name) db.add(db_user) db.commit() db.refresh(db_user) db.close() - - return UserResponse( - id=db_user.id, - email=db_user.email, - full_name=db_user.full_name - ) + return UserResponse(id=db_user.id, email=db_user.email, full_name=db_user.full_name, role=db_user.role) @app.post("/login", response_model=Token) def login(credentials: UserLogin): try: db = SessionLocal() - user = db.query(User).filter(User.email == credentials.email).first() db.close() - if not user: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="User not found" - ) - + raise HTTPException(status_code=401, detail="User not found") if not verify_password(credentials.password, user.password): - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid password" - ) - - access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + raise HTTPException(status_code=401, detail="Invalid password") access_token = create_access_token( - data={"sub": user.email}, expires_delta=access_token_expires + data={"sub": user.email}, + expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) ) - return Token(access_token=access_token, token_type="bearer") except HTTPException: raise except Exception as e: - print(f"ERROR in login: {str(e)}") - import traceback - traceback.print_exc() - raise HTTPException( - status_code=500, - detail=f"Server error: {str(e)}" - ) + raise HTTPException(status_code=500, detail=f"Server error: {str(e)}") @app.get("/me", response_model=UserResponse) def get_current_user(token: str): email = decode_token(token) if not email: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token" - ) - + raise HTTPException(status_code=401, detail="Invalid token") db = SessionLocal() user = db.query(User).filter(User.email == email).first() db.close() - if not user: - raise HTTPException( - status_code=status.HTTP_404_NOT_FOUND, - detail="User not found" - ) - - return UserResponse( - id=user.id, - email=user.email, - full_name=user.full_name - ) + raise HTTPException(status_code=404, detail="User not found") + return UserResponse(id=user.id, email=user.email, full_name=user.full_name, role=user.role) @app.get("/documents") def get_documents(): db = SessionLocal() - documents = db.query(Document).all() - result = [] - for doc in documents: result.append({ "id": doc.id, @@ -135,7 +100,116 @@ def get_documents(): "date": doc.meeting_date, "pdf_url": doc.pdf_url }) - db.close() + return result - return result \ No newline at end of file +# ── Migration colonne role ─────────────────────────────────── + +@app.post("/admin/migrate-add-role") +def migrate_add_role(): + """Endpoint temporaire — ajoute la colonne role si absente""" + db = SessionLocal() + try: + db.execute(text("ALTER TABLE users ADD COLUMN IF NOT EXISTS role VARCHAR DEFAULT 'user'")) + db.commit() + return {"message": "Colonne role ajoutée avec succès"} + except Exception as e: + return {"message": str(e)} + finally: + db.close() + +# ── Premier admin sans neckfire ────────────────────────────── + +@app.post("/admin/make-admin") +def make_admin(token: str, target_email: str): + """ + Passe un user en admin. + Fonctionne sans auth si aucun admin n'existe encore. + Se désactive automatiquement si un admin existe déjà. + """ + db = SessionLocal() + existing_admins = db.query(User).filter(User.role == "admin").count() + + if existing_admins > 0: + # Il y a déjà un admin — vérifier que l'appelant est admin + email = decode_token(token) + caller = db.query(User).filter(User.email == email).first() + if not caller or caller.role != "admin": + db.close() + raise HTTPException(status_code=403, detail="Accès refusé") + + target = db.query(User).filter(User.email == target_email).first() + if not target: + db.close() + raise HTTPException(status_code=404, detail="Utilisateur introuvable") + + target.role = "admin" + db.commit() + db.close() + return {"message": f"{target_email} est maintenant admin"} + +# ── Endpoints admin ────────────────────────────────────────── + +@app.get("/admin/stats") +def admin_stats(token: str): + require_admin(token) + db = SessionLocal() + total_docs = db.query(Document).count() + cir_docs = db.query(Document).filter(Document.source == "CIR").count() + sccs_docs = db.query(Document).filter(Document.source == "SCCS").count() + total_users = db.query(User).count() + db.close() + return { + "total_documents": total_docs, + "cir_documents": cir_docs, + "sccs_documents": sccs_docs, + "total_users": total_users + } + +@app.get("/admin/users") +def admin_get_users(token: str): + require_admin(token) + db = SessionLocal() + users = db.query(User).all() + result = [{"id": u.id, "email": u.email, "full_name": u.full_name, "role": u.role} for u in users] + db.close() + return result + +@app.delete("/admin/users/{user_id}") +def admin_delete_user(user_id: int, token: str): + require_admin(token) + db = SessionLocal() + user = db.query(User).filter(User.id == user_id).first() + if not user: + db.close() + raise HTTPException(status_code=404, detail="Utilisateur introuvable") + db.delete(user) + db.commit() + db.close() + return {"message": "Utilisateur supprimé"} + +@app.patch("/admin/users/{user_id}/role") +def admin_update_role(user_id: int, role_update: RoleUpdate, token: str): + require_admin(token) + db = SessionLocal() + user = db.query(User).filter(User.id == user_id).first() + if not user: + db.close() + raise HTTPException(status_code=404, detail="Utilisateur introuvable") + user.role = role_update.role + db.commit() + db.close() + return {"message": f"Rôle mis à jour : {role_update.role}"} + +@app.delete("/admin/documents/{doc_id}") +def admin_delete_document(doc_id: int, token: str): + require_admin(token) + db = SessionLocal() + doc = db.query(Document).filter(Document.id == doc_id).first() + if not doc: + db.close() + raise HTTPException(status_code=404, detail="Document introuvable") + db.delete(doc) + db.commit() + db.close() + return {"message": "Document supprimé"} \ No newline at end of file diff --git a/database/models.py b/database/models.py index be93f0b..6638365 100644 --- a/database/models.py +++ b/database/models.py @@ -3,19 +3,18 @@ from database.database import Base class User(Base): __tablename__ = "users" - - id = Column(Integer, primary_key=True, index=True) - email = Column(String, unique=True, index=True) - password = Column(String) + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True) + password = Column(String) full_name = Column(String, nullable=True) + role = Column(String, default="user") # ← nouveau class Document(Base): __tablename__ = "documents" - - id = Column(Integer, primary_key=True, index=True) - title = Column(String) - ingredient = Column(String) - source = Column(String) + id = Column(Integer, primary_key=True, index=True) + title = Column(String) + ingredient = Column(String) + source = Column(String) document_type = Column(String) - meeting_date = Column(String, nullable=True) - pdf_url = Column(String, unique=True) \ No newline at end of file + meeting_date = Column(String, nullable=True) + pdf_url = Column(String, unique=True) \ No newline at end of file