Add email field to users, login accepts email or username
This commit is contained in:
+16
-7
@@ -61,7 +61,7 @@ def require_admin(session: Optional[str] = Cookie(None)) -> dict:
|
|||||||
# --- Schemas ---
|
# --- Schemas ---
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
class LoginRequest(BaseModel):
|
||||||
username: str
|
username: str # accepts username or email
|
||||||
password: str
|
password: str
|
||||||
|
|
||||||
class InstanceCreate(BaseModel):
|
class InstanceCreate(BaseModel):
|
||||||
@@ -84,12 +84,14 @@ class RunCreate(BaseModel):
|
|||||||
|
|
||||||
class UserCreate(BaseModel):
|
class UserCreate(BaseModel):
|
||||||
username: str
|
username: str
|
||||||
|
email: str = ""
|
||||||
password: str
|
password: str
|
||||||
display_name: str = ""
|
display_name: str = ""
|
||||||
role: str = "user"
|
role: str = "user"
|
||||||
|
|
||||||
class UserUpdate(BaseModel):
|
class UserUpdate(BaseModel):
|
||||||
display_name: Optional[str] = None
|
display_name: Optional[str] = None
|
||||||
|
email: Optional[str] = None
|
||||||
role: Optional[str] = None
|
role: Optional[str] = None
|
||||||
password: Optional[str] = None
|
password: Optional[str] = None
|
||||||
|
|
||||||
@@ -114,12 +116,14 @@ class LLMProviderUpdate(BaseModel):
|
|||||||
|
|
||||||
@app.post("/api/login")
|
@app.post("/api/login")
|
||||||
def login(creds: LoginRequest, response: Response, db: Session = Depends(get_db)):
|
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):
|
if not user or not verify_password(creds.password, user.password_hash):
|
||||||
raise HTTPException(status_code=401, detail="Invalid credentials")
|
raise HTTPException(status_code=401, detail="Invalid credentials")
|
||||||
token = create_session(user)
|
token = create_session(user)
|
||||||
response.set_cookie("session", token, httponly=True, samesite="lax", max_age=86400 * 7)
|
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")
|
@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)):
|
def me(user: dict = Depends(require_auth), db: Session = Depends(get_db)):
|
||||||
u = db.query(User).filter(User.id == user["user_id"]).first()
|
u = db.query(User).filter(User.id == user["user_id"]).first()
|
||||||
return {
|
return {
|
||||||
"id": u.id, "username": u.username, "display_name": u.display_name,
|
"id": u.id, "username": u.username, "email": u.email or "",
|
||||||
"role": u.role, "created_at": u.created_at.isoformat() if u.created_at else None,
|
"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)):
|
def admin_list_users(admin: dict = Depends(require_admin), db: Session = Depends(get_db)):
|
||||||
users = db.query(User).all()
|
users = db.query(User).all()
|
||||||
return [{
|
return [{
|
||||||
"id": u.id, "username": u.username, "display_name": u.display_name,
|
"id": u.id, "username": u.username, "email": u.email or "",
|
||||||
"role": u.role, "created_at": u.created_at.isoformat() if u.created_at else None,
|
"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(),
|
"instance_count": db.query(AgentInstance).filter(AgentInstance.user_id == u.id).count(),
|
||||||
} for u in users]
|
} 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")
|
raise HTTPException(status_code=409, detail="Username exists")
|
||||||
user = User(
|
user = User(
|
||||||
username=data.username,
|
username=data.username,
|
||||||
|
email=data.email or None,
|
||||||
password_hash=hash_password(data.password),
|
password_hash=hash_password(data.password),
|
||||||
display_name=data.display_name or data.username,
|
display_name=data.display_name or data.username,
|
||||||
role=data.role,
|
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)
|
raise HTTPException(status_code=404)
|
||||||
if update.display_name is not None:
|
if update.display_name is not None:
|
||||||
user.display_name = update.display_name
|
user.display_name = update.display_name
|
||||||
|
if update.email is not None:
|
||||||
|
user.email = update.email or None
|
||||||
if update.role is not None:
|
if update.role is not None:
|
||||||
user.role = update.role
|
user.role = update.role
|
||||||
if update.password is not None:
|
if update.password is not None:
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ class User(Base):
|
|||||||
|
|
||||||
id = Column(Integer, primary_key=True, autoincrement=True)
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
||||||
username = Column(String, unique=True, nullable=False)
|
username = Column(String, unique=True, nullable=False)
|
||||||
|
email = Column(String, unique=True, nullable=True)
|
||||||
password_hash = Column(String, nullable=False)
|
password_hash = Column(String, nullable=False)
|
||||||
display_name = Column(String, default="")
|
display_name = Column(String, default="")
|
||||||
role = Column(String, default="user") # admin or user
|
role = Column(String, default="user") # admin or user
|
||||||
|
|||||||
@@ -74,16 +74,20 @@ tr:hover td{background:var(--surface2)}
|
|||||||
<h3>Create User</h3>
|
<h3>Create User</h3>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
<div class="field"><label>Username</label><input id="nu-user" placeholder="username"></div>
|
<div class="field"><label>Username</label><input id="nu-user" placeholder="username"></div>
|
||||||
<div class="field"><label>Password</label><input type="password" id="nu-pass"></div>
|
<div class="field"><label>Email</label><input type="email" id="nu-email" placeholder="user@example.com"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-row">
|
<div class="form-row">
|
||||||
|
<div class="field"><label>Password</label><input type="password" id="nu-pass"></div>
|
||||||
<div class="field"><label>Display Name</label><input id="nu-name" placeholder="Full Name"></div>
|
<div class="field"><label>Display Name</label><input id="nu-name" placeholder="Full Name"></div>
|
||||||
|
</div>
|
||||||
|
<div class="form-row">
|
||||||
<div class="field"><label>Role</label><select id="nu-role"><option value="user">User</option><option value="admin">Admin</option></select></div>
|
<div class="field"><label>Role</label><select id="nu-role"><option value="user">User</option><option value="admin">Admin</option></select></div>
|
||||||
|
<div class="field"></div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-primary" onclick="createUser()">Create</button>
|
<button class="btn btn-primary" onclick="createUser()">Create</button>
|
||||||
<span class="msg" id="nu-msg"></span>
|
<span class="msg" id="nu-msg"></span>
|
||||||
</div>
|
</div>
|
||||||
<table id="users-table"><thead><tr><th>Username</th><th>Display Name</th><th>Role</th><th>Agents</th><th>Actions</th></tr></thead><tbody></tbody></table>
|
<table id="users-table"><thead><tr><th>Username</th><th>Email</th><th>Display Name</th><th>Role</th><th>Agents</th><th>Actions</th></tr></thead><tbody></tbody></table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Catalog -->
|
<!-- Catalog -->
|
||||||
@@ -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 res=await fetch(API+'/api/admin/users');if(res.status===403||res.status===401){location.href='/';return}
|
||||||
const users=await res.json();
|
const users=await res.json();
|
||||||
document.querySelector('#users-table tbody').innerHTML=users.map(u=>`<tr>
|
document.querySelector('#users-table tbody').innerHTML=users.map(u=>`<tr>
|
||||||
<td>${u.username}</td><td>${u.display_name}</td><td><span class="badge ${u.role}">${u.role}</span></td>
|
<td>${u.username}</td><td style="font-size:.8rem">${u.email||'<span style="color:var(--yellow)">none</span>'}</td><td>${u.display_name}</td><td><span class="badge ${u.role}">${u.role}</span></td>
|
||||||
<td>${u.instance_count}</td>
|
<td>${u.instance_count}</td>
|
||||||
<td><div class="action-btns"><button class="btn btn-danger btn-sm" onclick="deleteUser(${u.id},'${u.username}')">Delete</button></div></td>
|
<td><div class="action-btns"><button class="btn btn-danger btn-sm" onclick="deleteUser(${u.id},'${u.username}')">Delete</button></div></td>
|
||||||
</tr>`).join('');
|
</tr>`).join('');
|
||||||
}
|
}
|
||||||
async function createUser(){
|
async function createUser(){
|
||||||
const res=await fetch(API+'/api/admin/users',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({
|
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})});
|
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)}
|
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()}
|
async function deleteUser(id,name){if(!confirm('Delete user '+name+'?'))return;await fetch(API+'/api/admin/users/'+id,{method:'DELETE'});loadUsers()}
|
||||||
|
|||||||
Reference in New Issue
Block a user