Nginx now listens for path /api and redirects internally, changed BaseURL to use /api, AND patched security authentication issue for POST and GET requests to the uvicorn service
This commit is contained in:
+3
-4
@@ -63,10 +63,9 @@
|
||||
- **API client:** Added `api.changePassword(id, oldPwd, newPwd)` that surfaces the backend's `detail` message inline rather than just the HTTP status text.
|
||||
- **Frontend:** Wired the previously-inert "Update password" button in `SettingsScreen` — submits via `onChangePassword`, shows inline error / success states, disables while in flight, clears the fields on success, and writes a `password_changed` audit entry.
|
||||
- **Defaults confirmed:** Seeded users (`seed.py`) and admin-created users (`app.jsx`) both default to `password123`.
|
||||
15. **Real Login Authentication:** Fixed a security bug where the login screen accepted any password.
|
||||
- The password input on `LoginScreen` was a decorative `defaultValue` field — the button submitted with no password, and `onLogin` had a fallback default of `"password123"` which matched every seeded account.
|
||||
- Bound the input to component state, send the actual typed password to `api.login`, and let backend `401`s propagate so the screen can render an inline "Incorrect password" message instead of silently letting anyone in.
|
||||
- Enter key now submits, and the button disables while the request is in flight.
|
||||
15. **Real Login Authentication:** Fixed a security bug where the login screen accepted any password. Bound the input to component state and implemented proper 401 handling with inline error messaging.
|
||||
16. **Network Hardening:** Configured the frontend to use a relative `/api` path, allowing the FastAPI backend to be completely shielded behind an Nginx SSL reverse proxy on `127.0.0.1`. No internal ports are now exposed to the public internet.
|
||||
17. **API Authentication Enforcement:** Fixed a security vulnerability where API endpoints were publicly accessible without a token. Implemented the `get_current_user` dependency in `backend/auth.py` and applied it to all sensitive routes. Accessing `/tasks`, `/users`, etc. now strictly requires a valid JWT Bearer token.
|
||||
|
||||
### Phase 3: Advanced Features
|
||||
- **Real-time Notifications:** Explore WebSockets for task assignments.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
class ApiService {
|
||||
constructor() {
|
||||
this.baseUrl = `http://${window.location.hostname}:24024`;
|
||||
this.baseUrl = '/api';
|
||||
this.token = localStorage.getItem('dashy_token');
|
||||
this.subscribers = new Set();
|
||||
}
|
||||
|
||||
+25
-1
@@ -2,12 +2,17 @@ from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
from fastapi import Depends, HTTPException, status
|
||||
from fastapi.security import OAuth2PasswordBearer
|
||||
from sqlalchemy.orm import Session
|
||||
from . import models, database
|
||||
|
||||
# SECRET_KEY should be in an environment variable in production
|
||||
SECRET_KEY = "your-secret-key-change-this-in-production"
|
||||
ALGORITHM = "HS256"
|
||||
ACCESS_TOKEN_EXPIRE_MINUTES = 30
|
||||
|
||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="token")
|
||||
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
||||
|
||||
def verify_password(plain_password, hashed_password):
|
||||
@@ -21,7 +26,26 @@ def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=15)
|
||||
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(database.get_db)):
|
||||
credentials_exception = HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED,
|
||||
detail="Could not validate credentials",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
try:
|
||||
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
||||
user_id: str = payload.get("sub")
|
||||
if user_id is None:
|
||||
raise credentials_exception
|
||||
except JWTError:
|
||||
raise credentials_exception
|
||||
|
||||
user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||
if user is None:
|
||||
raise credentials_exception
|
||||
return user
|
||||
|
||||
+11
-11
@@ -32,11 +32,11 @@ async def login_for_access_token(form_data: schemas.UserLogin, db: Session = Dep
|
||||
return {"access_token": access_token, "token_type": "bearer"}
|
||||
|
||||
@app.get("/users", response_model=List[schemas.User])
|
||||
def read_users(db: Session = Depends(get_db)):
|
||||
def read_users(db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
return db.query(models.User).all()
|
||||
|
||||
@app.post("/users", response_model=schemas.User)
|
||||
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
||||
def create_user(user: schemas.UserCreate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
db_user = models.User(
|
||||
id=user.id,
|
||||
name=user.name,
|
||||
@@ -55,7 +55,7 @@ def create_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
||||
return db_user
|
||||
|
||||
@app.patch("/users/{user_id}", response_model=schemas.User)
|
||||
def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Depends(get_db)):
|
||||
def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
db_user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
@@ -69,7 +69,7 @@ def update_user(user_id: str, user_update: schemas.UserUpdate, db: Session = Dep
|
||||
return db_user
|
||||
|
||||
@app.post("/users/{user_id}/password")
|
||||
def change_password(user_id: str, payload: schemas.PasswordChange, db: Session = Depends(get_db)):
|
||||
def change_password(user_id: str, payload: schemas.PasswordChange, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
db_user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
@@ -80,7 +80,7 @@ def change_password(user_id: str, payload: schemas.PasswordChange, db: Session =
|
||||
return {"message": "Password updated"}
|
||||
|
||||
@app.delete("/users/{user_id}")
|
||||
def delete_user(user_id: str, db: Session = Depends(get_db)):
|
||||
def delete_user(user_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
db_user = db.query(models.User).filter(models.User.id == user_id).first()
|
||||
if not db_user:
|
||||
raise HTTPException(status_code=404, detail="User not found")
|
||||
@@ -93,11 +93,11 @@ def delete_user(user_id: str, db: Session = Depends(get_db)):
|
||||
return {"message": "User deleted"}
|
||||
|
||||
@app.get("/tasks", response_model=List[schemas.Task])
|
||||
def read_tasks(db: Session = Depends(get_db)):
|
||||
def read_tasks(db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
return db.query(models.Task).all()
|
||||
|
||||
@app.post("/tasks", response_model=schemas.Task)
|
||||
def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db)):
|
||||
def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
task_id = task.id or f"t_{uuid.uuid4().hex[:8]}"
|
||||
db_task = models.Task(
|
||||
id=task_id,
|
||||
@@ -125,7 +125,7 @@ def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db)):
|
||||
return db_task
|
||||
|
||||
@app.patch("/tasks/{task_id}", response_model=schemas.Task)
|
||||
def update_task(task_id: str, task_update: schemas.TaskUpdate, db: Session = Depends(get_db)):
|
||||
def update_task(task_id: str, task_update: schemas.TaskUpdate, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
db_task = db.query(models.Task).filter(models.Task.id == task_id).first()
|
||||
if not db_task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
@@ -139,7 +139,7 @@ def update_task(task_id: str, task_update: schemas.TaskUpdate, db: Session = Dep
|
||||
return db_task
|
||||
|
||||
@app.delete("/tasks/{task_id}")
|
||||
def delete_task(task_id: str, db: Session = Depends(get_db)):
|
||||
def delete_task(task_id: str, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
db_task = db.query(models.Task).filter(models.Task.id == task_id).first()
|
||||
if not db_task:
|
||||
raise HTTPException(status_code=404, detail="Task not found")
|
||||
@@ -148,11 +148,11 @@ def delete_task(task_id: str, db: Session = Depends(get_db)):
|
||||
db.commit()
|
||||
return {"message": "Task deleted"}
|
||||
@app.get("/audit", response_model=List[schemas.AuditLog])
|
||||
def read_audit(db: Session = Depends(get_db)):
|
||||
def read_audit(db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
return db.query(models.AuditLog).order_by(models.AuditLog.at.desc()).all()
|
||||
|
||||
@app.post("/audit", response_model=schemas.AuditLog)
|
||||
def create_audit(audit: schemas.AuditLogBase, db: Session = Depends(get_db)):
|
||||
def create_audit(audit: schemas.AuditLogBase, db: Session = Depends(get_db), current_user: models.User = Depends(auth.get_current_user)):
|
||||
audit_id = f"a_{uuid.uuid4().hex[:8]}"
|
||||
db_audit = models.AuditLog(
|
||||
id=audit_id,
|
||||
|
||||
Reference in New Issue
Block a user