mirror of
https://github.com/myronblair/kino-app
synced 2026-07-28 05:23:45 -05:00
Add self-service admin password change (requires current password)
This commit is contained in:
@@ -20,6 +20,11 @@ class UserLogin(BaseModel):
|
|||||||
password: str
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class PasswordChange(BaseModel):
|
||||||
|
current_password: str
|
||||||
|
new_password: str
|
||||||
|
|
||||||
|
|
||||||
class UserPublic(BaseModel):
|
class UserPublic(BaseModel):
|
||||||
model_config = ConfigDict(extra="ignore")
|
model_config = ConfigDict(extra="ignore")
|
||||||
id: str
|
id: str
|
||||||
|
|||||||
+14
-1
@@ -15,7 +15,7 @@ from starlette.middleware.cors import CORSMiddleware
|
|||||||
from motor.motor_asyncio import AsyncIOMotorClient
|
from motor.motor_asyncio import AsyncIOMotorClient
|
||||||
|
|
||||||
from models import (
|
from models import (
|
||||||
UserCreate, UserLogin, UserPublic, TokenResponse,
|
UserCreate, UserLogin, UserPublic, TokenResponse, PasswordChange,
|
||||||
Profile, ProfileCreate, ProfileUpdate, RATING_ORDER,
|
Profile, ProfileCreate, ProfileUpdate, RATING_ORDER,
|
||||||
Movie, MovieCreate, MovieUpdate,
|
Movie, MovieCreate, MovieUpdate,
|
||||||
Subtitle,
|
Subtitle,
|
||||||
@@ -235,6 +235,19 @@ async def me(user: dict = Depends(get_current_user)):
|
|||||||
return _user_public(user)
|
return _user_public(user)
|
||||||
|
|
||||||
|
|
||||||
|
@api.post("/auth/change-password")
|
||||||
|
async def change_password(payload: PasswordChange, user: dict = Depends(get_current_user)):
|
||||||
|
full_user = await db.users.find_one({"id": user["id"]})
|
||||||
|
if not full_user or not verify_password(payload.current_password, full_user["password_hash"]):
|
||||||
|
raise HTTPException(status_code=401, detail="Current password is incorrect")
|
||||||
|
if len(payload.new_password) < 8:
|
||||||
|
raise HTTPException(status_code=400, detail="New password must be at least 8 characters")
|
||||||
|
await db.users.update_one(
|
||||||
|
{"id": user["id"]}, {"$set": {"password_hash": hash_password(payload.new_password)}}
|
||||||
|
)
|
||||||
|
return {"ok": True}
|
||||||
|
|
||||||
|
|
||||||
# ============ PROFILES ============
|
# ============ PROFILES ============
|
||||||
@api.get("/profiles", response_model=List[Profile])
|
@api.get("/profiles", response_model=List[Profile])
|
||||||
async def list_profiles(user: dict = Depends(get_current_user)):
|
async def list_profiles(user: dict = Depends(get_current_user)):
|
||||||
|
|||||||
@@ -51,6 +51,9 @@ export default function Settings() {
|
|||||||
const [traktStatus, setTraktStatus] = useState({ connected: false, username: "" });
|
const [traktStatus, setTraktStatus] = useState({ connected: false, username: "" });
|
||||||
const [traktModal, setTraktModal] = useState(null);
|
const [traktModal, setTraktModal] = useState(null);
|
||||||
const [show, setShow] = useState({});
|
const [show, setShow] = useState({});
|
||||||
|
const [pw, setPw] = useState({ current_password: "", new_password: "", confirm: "" });
|
||||||
|
const [pwShow, setPwShow] = useState(false);
|
||||||
|
const [pwSaving, setPwSaving] = useState(false);
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const [{ data }, { data: ts }] = await Promise.all([api.get("/settings"), api.get("/trakt/status").catch(() => ({ data: { connected: false } }))]);
|
const [{ data }, { data: ts }] = await Promise.all([api.get("/settings"), api.get("/trakt/status").catch(() => ({ data: { connected: false } }))]);
|
||||||
@@ -97,6 +100,19 @@ export default function Settings() {
|
|||||||
await api.delete("/trakt/disconnect"); toast.success("Trakt disconnected"); load();
|
await api.delete("/trakt/disconnect"); toast.success("Trakt disconnected"); load();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const changePassword = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (pw.new_password.length < 8) { toast.error("New password must be at least 8 characters"); return; }
|
||||||
|
if (pw.new_password !== pw.confirm) { toast.error("New passwords don't match"); return; }
|
||||||
|
setPwSaving(true);
|
||||||
|
try {
|
||||||
|
await api.post("/auth/change-password", { current_password: pw.current_password, new_password: pw.new_password });
|
||||||
|
toast.success("Password changed");
|
||||||
|
setPw({ current_password: "", new_password: "", confirm: "" });
|
||||||
|
} catch (err) { toast.error(err.response?.data?.detail || "Could not change password"); }
|
||||||
|
finally { setPwSaving(false); }
|
||||||
|
};
|
||||||
|
|
||||||
const enrichAll = async () => {
|
const enrichAll = async () => {
|
||||||
if (!window.confirm("Sweep all movies with missing metadata and fill from TMDB? This may take a while.")) return;
|
if (!window.confirm("Sweep all movies with missing metadata and fill from TMDB? This may take a while.")) return;
|
||||||
try { const { data } = await api.post("/tmdb/enrich-all"); toast.success(`Enriched ${data.enriched}, skipped ${data.skipped}, failed ${data.failed}`); }
|
try { const { data } = await api.post("/tmdb/enrich-all"); toast.success(`Enriched ${data.enriched}, skipped ${data.skipped}, failed ${data.failed}`); }
|
||||||
@@ -110,6 +126,49 @@ export default function Settings() {
|
|||||||
<span className="text-xs uppercase tracking-[0.3em] text-[#D9381E]">Admin</span>
|
<span className="text-xs uppercase tracking-[0.3em] text-[#D9381E]">Admin</span>
|
||||||
<h1 className="font-display text-5xl md:text-6xl font-black tracking-tighter text-white mt-3">Settings</h1>
|
<h1 className="font-display text-5xl md:text-6xl font-black tracking-tighter text-white mt-3">Settings</h1>
|
||||||
|
|
||||||
|
<form onSubmit={changePassword} className="mt-12 space-y-4 pb-10 border-b border-[#222]" data-testid="change-password-form">
|
||||||
|
<h2 className="font-display text-2xl font-bold text-white">Admin Password</h2>
|
||||||
|
<p className="text-sm text-[#8A8A8A]">Requires your current password. Changing it locks out anyone who only had the old one.</p>
|
||||||
|
<label className="block">
|
||||||
|
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">Current password</span>
|
||||||
|
<div className="relative mt-2">
|
||||||
|
<input
|
||||||
|
type={pwShow ? "text" : "password"}
|
||||||
|
value={pw.current_password}
|
||||||
|
onChange={(e) => setPw({ ...pw, current_password: e.target.value })}
|
||||||
|
className="w-full bg-[#0F0F0F] border border-[#222] focus:border-[#D9381E] focus:outline-none text-white px-4 py-3 pr-12"
|
||||||
|
data-testid="current-password"
|
||||||
|
/>
|
||||||
|
<button type="button" onClick={() => setPwShow(!pwShow)} className="absolute right-3 top-1/2 -translate-y-1/2 text-[#8A8A8A] hover:text-white">
|
||||||
|
{pwShow ? <EyeOff size={16} strokeWidth={1.5} /> : <Eye size={16} strokeWidth={1.5} />}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label className="block">
|
||||||
|
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">New password (min 8 characters)</span>
|
||||||
|
<input
|
||||||
|
type={pwShow ? "text" : "password"}
|
||||||
|
value={pw.new_password}
|
||||||
|
onChange={(e) => setPw({ ...pw, new_password: e.target.value })}
|
||||||
|
className="mt-2 w-full bg-[#0F0F0F] border border-[#222] focus:border-[#D9381E] focus:outline-none text-white px-4 py-3"
|
||||||
|
data-testid="new-password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label className="block">
|
||||||
|
<span className="text-[10px] uppercase tracking-[0.3em] text-[#8A8A8A]">Confirm new password</span>
|
||||||
|
<input
|
||||||
|
type={pwShow ? "text" : "password"}
|
||||||
|
value={pw.confirm}
|
||||||
|
onChange={(e) => setPw({ ...pw, confirm: e.target.value })}
|
||||||
|
className="mt-2 w-full bg-[#0F0F0F] border border-[#222] focus:border-[#D9381E] focus:outline-none text-white px-4 py-3"
|
||||||
|
data-testid="confirm-password"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<button type="submit" disabled={pwSaving} className="bg-[#D9381E] hover:bg-[#ED4B32] disabled:opacity-60 text-white px-8 py-3 text-sm uppercase tracking-[0.2em]" data-testid="change-password-button">
|
||||||
|
{pwSaving ? "Changing…" : "Change Password"}
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
<form onSubmit={save} className="mt-12 space-y-10" data-testid="settings-form">
|
<form onSubmit={save} className="mt-12 space-y-10" data-testid="settings-form">
|
||||||
{/* TMDB */}
|
{/* TMDB */}
|
||||||
<section>
|
<section>
|
||||||
|
|||||||
Reference in New Issue
Block a user