feat: add ability to change password

This commit is contained in:
julius 2025-03-16 18:11:28 +01:00
parent 39630725a4
commit 4252e737d7
Signed by: julius
GPG Key ID: C80A63E6A5FD7092
6 changed files with 127 additions and 57 deletions

View File

@ -256,22 +256,28 @@ async def set_first_password(req: FirstPassword):
raise credentials_exception raise credentials_exception
class ChangedPassword(BaseModel):
current_password: str
new_password: str
async def change_password( async def change_password(
current_password: str, request: ChangedPassword,
new_password: str,
user: Annotated[Player, Depends(get_current_active_user)], user: Annotated[Player, Depends(get_current_active_user)],
): ):
if ( if (
new_password request.new_password
and user.hashed_password and user.hashed_password
and verify_password(current_password, user.hashed_password) and verify_password(request.current_password, user.hashed_password)
): ):
with Session(engine) as session: with Session(engine) as session:
user.hashed_password = get_password_hash(new_password) user.hashed_password = get_password_hash(request.new_password)
session.add(user) session.add(user)
session.commit() session.commit()
return PlainTextResponse( return PlainTextResponse(
"Password changed successfully", status_code=status.HTTP_200_OK "Password changed successfully",
status_code=status.HTTP_200_OK,
media_type="text/plain",
) )
else: else:
raise HTTPException( raise HTTPException(

View File

@ -153,7 +153,8 @@ input:checked+.slider:before {
input { input {
padding: 0.2em 16px; padding: 0.2em 16px;
margin-bottom: 0.5em; margin-top: 0.25em;
margin-bottom: 0.25em;
border-radius: 1em; border-radius: 1em;
} }

View File

@ -36,6 +36,7 @@ function App() {
<Route path="/network" element={<GraphComponent />} /> <Route path="/network" element={<GraphComponent />} />
<Route path="/analysis" element={<Analysis />} /> <Route path="/analysis" element={<Analysis />} />
<Route path="/mvp" element={<MVPChart />} /> <Route path="/mvp" element={<MVPChart />} />
<Route path="/changepassword" element={<SetPassword />} />
</Routes> </Routes>
<Footer /> <Footer />
</SessionProvider> </SessionProvider>

View File

@ -3,6 +3,7 @@ import { useSession } from "./Session";
import { User } from "./api"; import { User } from "./api";
import { useTheme } from "./ThemeProvider"; import { useTheme } from "./ThemeProvider";
import { colourTheme, darkTheme, normalTheme, rainbowTheme } from "./themes"; import { colourTheme, darkTheme, normalTheme, rainbowTheme } from "./themes";
import { useNavigate } from "react-router";
interface ContextMenuItem { interface ContextMenuItem {
label: string; label: string;
@ -35,6 +36,7 @@ const UserInfo = (user: User) => {
export default function Avatar() { export default function Avatar() {
const { user, onLogout } = useSession(); const { user, onLogout } = useSession();
const { theme, setTheme } = useTheme(); const { theme, setTheme } = useTheme();
const navigate = useNavigate();
const [contextMenu, setContextMenu] = useState<{ const [contextMenu, setContextMenu] = useState<{
open: boolean; open: boolean;
allowOpen: boolean; allowOpen: boolean;
@ -46,6 +48,7 @@ export default function Avatar() {
const contextMenuItems: ContextMenuItem[] = [ const contextMenuItems: ContextMenuItem[] = [
{ label: "view Profile", onClick: handleViewProfile }, { label: "view Profile", onClick: handleViewProfile },
{ label: "change password", onClick: () => navigate("/changepassword") },
{ {
label: "change theme", label: "change theme",
onClick: () => { onClick: () => {

View File

@ -1,10 +1,9 @@
import { jwtDecode, JwtPayload } from "jwt-decode"; import { jwtDecode, JwtPayload } from "jwt-decode";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { baseUrl } from "./api"; import { apiAuth, baseUrl } from "./api";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import eye from "./eye.svg";
import { Eye, EyeSlash } from "./Icons"; import { Eye, EyeSlash } from "./Icons";
import { relative } from "path"; import { useSession } from "./Session";
interface SetPassToken extends JwtPayload { interface SetPassToken extends JwtPayload {
name: string; name: string;
@ -13,6 +12,7 @@ interface SetPassToken extends JwtPayload {
export const SetPassword = () => { export const SetPassword = () => {
const [name, setName] = useState("after getting your token."); const [name, setName] = useState("after getting your token.");
const [username, setUsername] = useState(""); const [username, setUsername] = useState("");
const [currentPassword, setCurrentPassword] = useState("");
const [password, setPassword] = useState(""); const [password, setPassword] = useState("");
const [passwordr, setPasswordr] = useState(""); const [passwordr, setPasswordr] = useState("");
const [token, setToken] = useState(""); const [token, setToken] = useState("");
@ -20,20 +20,26 @@ export const SetPassword = () => {
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const { user } = useSession();
useEffect(() => { useEffect(() => {
const params = new URLSearchParams(window.location.search); if (user) {
const token = params.get("token"); setUsername(user.username);
if (token) { setName(user.display_name);
setToken(token); } else {
try { const params = new URLSearchParams(window.location.search);
const payload = jwtDecode<SetPassToken>(token); const token = params.get("token");
if (payload.name) setName(payload.name); if (token) {
else if (payload.sub) setName(payload.sub); setToken(token);
else setName("Mr. I-have-no Token"); try {
payload.sub && setUsername(payload.sub); const payload = jwtDecode<SetPassToken>(token);
} catch (InvalidTokenError) { if (payload.name) setName(payload.name);
setName("Mr. I-have-no-valid Token"); else if (payload.sub) setName(payload.sub);
else setName("Mr. I-have-no Token");
payload.sub && setUsername(payload.sub);
} catch (InvalidTokenError) {
setName("Mr. I-have-no-valid Token");
}
} }
} }
}, []); }, []);
@ -42,35 +48,49 @@ export const SetPassword = () => {
e.preventDefault(); e.preventDefault();
if (password === passwordr) { if (password === passwordr) {
setLoading(true); setLoading(true);
const req = new Request(`${baseUrl}api/set_password`, { if (user) {
method: "POST", const resp = await apiAuth(
headers: { "player/change_password",
"Content-Type": "application/json", { current_password: currentPassword, new_password: password },
}, "POST"
body: JSON.stringify({ token: token, password: password }), );
}); setLoading(false);
let resp: Response; if (resp.detail) setError(resp.detail);
try { else {
resp = await fetch(req); setError(resp);
} catch (e) { setTimeout(() => navigate("/"), 2000);
throw new Error(`request failed: ${e}`); }
} } else {
setLoading(false); const req = new Request(`${baseUrl}api/set_password`, {
method: "POST",
if (resp.ok) { headers: {
console.log(resp); "Content-Type": "application/json",
navigate("/", { },
replace: true, body: JSON.stringify({ token: token, password: password }),
state: { username: username, password: password },
}); });
} let resp: Response;
try {
resp = await fetch(req);
} catch (e) {
throw new Error(`request failed: ${e}`);
}
setLoading(false);
if (!resp.ok) { if (resp.ok) {
if (resp.status === 401) { console.log(resp);
const { detail } = await resp.json(); navigate("/", {
if (detail) setError(detail); replace: true,
else setError("unauthorized"); state: { username: username, password: password },
throw new Error("Unauthorized"); });
}
if (!resp.ok) {
if (resp.status === 401) {
const { detail } = await resp.json();
if (detail) setError(detail);
else setError("unauthorized");
throw new Error("Unauthorized");
}
} }
} }
} else setError("passwords are not the same"); } else setError("passwords are not the same");
@ -78,12 +98,16 @@ export const SetPassword = () => {
return ( return (
<> <>
<h2> {user ? (
set your password, <h2> change your password </h2>
<br /> ) : (
{name} <h2>
</h2> set your password,
{username && ( <br />
{name}
</h2>
)}
{!user && username && (
<span> <span>
your username is: <i>{username}</i> your username is: <i>{username}</i>
</span> </span>
@ -104,6 +128,30 @@ export const SetPassword = () => {
flexDirection: "column", flexDirection: "column",
}} }}
> >
{user && (
<div>
<input
type={visible ? "text" : "password"}
id="password"
name="password"
placeholder="current password"
minLength={8}
value={currentPassword}
required
onChange={(evt) => {
setError("");
setCurrentPassword(evt.target.value);
}}
/>
<hr
style={{
margin: "8px",
borderStyle: "inset",
display: "block",
}}
/>
</div>
)}
<div> <div>
<input <input
type={visible ? "text" : "password"} type={visible ? "text" : "password"}
@ -148,7 +196,7 @@ export const SetPassword = () => {
</div> </div>
<div>{error && <span style={{ color: "red" }}>{error}</span>}</div> <div>{error && <span style={{ color: "red" }}>{error}</span>}</div>
<button type="submit" value="login" style={{ fontSize: "small" }}> <button type="submit" value="login" style={{ fontSize: "small" }}>
login submit
</button> </button>
{loading && <span className="loader" />} {loading && <span className="loader" />}
</form> </form>

View File

@ -29,7 +29,18 @@ export async function apiAuth(
throw new Error("Unauthorized"); throw new Error("Unauthorized");
} }
} }
return resp.json(); const contentType = resp.headers.get("Content-Type");
switch (contentType) {
case "application/json":
return resp.json();
case "text/plain":
case "text/plain; charset=utf-8":
return resp.text();
case null:
return null;
default:
throw new Error(`Unsupported content type: ${contentType}`);
}
} }
export type User = { export type User = {