diff --git a/dashboard/app.py b/dashboard/app.py index a5557d0..cb42686 100644 --- a/dashboard/app.py +++ b/dashboard/app.py @@ -61,7 +61,7 @@ def require_admin(session: Optional[str] = Cookie(None)) -> dict: # --- Schemas --- class LoginRequest(BaseModel): - username: str + username: str # accepts username or email password: str class InstanceCreate(BaseModel): @@ -84,12 +84,14 @@ class RunCreate(BaseModel): class UserCreate(BaseModel): username: str + email: str = "" password: str display_name: str = "" role: str = "user" class UserUpdate(BaseModel): display_name: Optional[str] = None + email: Optional[str] = None role: Optional[str] = None password: Optional[str] = None @@ -114,12 +116,14 @@ class LLMProviderUpdate(BaseModel): @app.post("/api/login") def login(creds: LoginRequest, response: Response, db: Session = Depends(get_db)): - user = db.query(User).filter(User.username == creds.username).first() + user = db.query(User).filter( + (User.username == creds.username) | (User.email == creds.username) + ).first() if not user or not verify_password(creds.password, user.password_hash): raise HTTPException(status_code=401, detail="Invalid credentials") token = create_session(user) response.set_cookie("session", token, httponly=True, samesite="lax", max_age=86400 * 7) - return {"status": "ok", "user": user.username, "role": user.role, "display_name": user.display_name} + return {"status": "ok", "user": user.username, "email": user.email or "", "role": user.role, "display_name": user.display_name} @app.post("/api/logout") @@ -134,8 +138,9 @@ def logout(response: Response, session: Optional[str] = Cookie(None)): def me(user: dict = Depends(require_auth), db: Session = Depends(get_db)): u = db.query(User).filter(User.id == user["user_id"]).first() return { - "id": u.id, "username": u.username, "display_name": u.display_name, - "role": u.role, "created_at": u.created_at.isoformat() if u.created_at else None, + "id": u.id, "username": u.username, "email": u.email or "", + "display_name": u.display_name, "role": u.role, + "created_at": u.created_at.isoformat() if u.created_at else None, } @@ -329,8 +334,9 @@ def list_runs(limit: int = 50, user: dict = Depends(require_auth), db: Session = def admin_list_users(admin: dict = Depends(require_admin), db: Session = Depends(get_db)): users = db.query(User).all() return [{ - "id": u.id, "username": u.username, "display_name": u.display_name, - "role": u.role, "created_at": u.created_at.isoformat() if u.created_at else None, + "id": u.id, "username": u.username, "email": u.email or "", + "display_name": u.display_name, "role": u.role, + "created_at": u.created_at.isoformat() if u.created_at else None, "instance_count": db.query(AgentInstance).filter(AgentInstance.user_id == u.id).count(), } for u in users] @@ -341,6 +347,7 @@ def admin_create_user(data: UserCreate, admin: dict = Depends(require_admin), db raise HTTPException(status_code=409, detail="Username exists") user = User( username=data.username, + email=data.email or None, password_hash=hash_password(data.password), display_name=data.display_name or data.username, role=data.role, @@ -357,6 +364,8 @@ def admin_update_user(user_id: int, update: UserUpdate, admin: dict = Depends(re raise HTTPException(status_code=404) if update.display_name is not None: user.display_name = update.display_name + if update.email is not None: + user.email = update.email or None if update.role is not None: user.role = update.role if update.password is not None: diff --git a/dashboard/models.py b/dashboard/models.py index 142072c..33e62e4 100644 --- a/dashboard/models.py +++ b/dashboard/models.py @@ -9,6 +9,7 @@ class User(Base): id = Column(Integer, primary_key=True, autoincrement=True) username = Column(String, unique=True, nullable=False) + email = Column(String, unique=True, nullable=True) password_hash = Column(String, nullable=False) display_name = Column(String, default="") role = Column(String, default="user") # admin or user diff --git a/dashboard/static/admin.html b/dashboard/static/admin.html index 2f2bbe2..ec8c17d 100644 --- a/dashboard/static/admin.html +++ b/dashboard/static/admin.html @@ -74,16 +74,20 @@ tr:hover td{background:var(--surface2)}

Create User

-
+
+
+
+
+
-
UsernameDisplay NameRoleAgentsActions
+
UsernameEmailDisplay NameRoleAgentsActions
@@ -154,16 +158,17 @@ async function loadUsers(){ const res=await fetch(API+'/api/admin/users');if(res.status===403||res.status===401){location.href='/';return} const users=await res.json(); document.querySelector('#users-table tbody').innerHTML=users.map(u=>` - ${u.username}${u.display_name}${u.role} + ${u.username}${u.email||'none'}${u.display_name}${u.role} ${u.instance_count}
`).join(''); } async function createUser(){ const res=await fetch(API+'/api/admin/users',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({ - username:document.getElementById('nu-user').value,password:document.getElementById('nu-pass').value, + username:document.getElementById('nu-user').value,email:document.getElementById('nu-email').value, + password:document.getElementById('nu-pass').value, display_name:document.getElementById('nu-name').value,role:document.getElementById('nu-role').value})}); - if(res.ok){showMsg('nu-msg','Created',true);loadUsers();document.getElementById('nu-user').value='';document.getElementById('nu-pass').value='';document.getElementById('nu-name').value=''} + if(res.ok){showMsg('nu-msg','Created',true);loadUsers();['nu-user','nu-email','nu-pass','nu-name'].forEach(id=>document.getElementById(id).value='')} else{const e=await res.json();showMsg('nu-msg',e.detail||'Error',false)} } async function deleteUser(id,name){if(!confirm('Delete user '+name+'?'))return;await fetch(API+'/api/admin/users/'+id,{method:'DELETE'});loadUsers()}