From 89d032dd69344f0e3420e9f505469337d83d8754 Mon Sep 17 00:00:00 2001 From: ribardej Date: Tue, 14 Oct 2025 11:34:25 +0200 Subject: [PATCH 1/4] feat(frontend): introduced a working frontend prototype --- 7project/frontend/src/App.tsx | 54 +++--- 7project/frontend/src/api.ts | 100 +++++++++++ 7project/frontend/src/pages/Dashboard.tsx | 155 ++++++++++++++++++ .../frontend/src/pages/LoginRegisterPage.tsx | 71 ++++++++ 4 files changed, 347 insertions(+), 33 deletions(-) create mode 100644 7project/frontend/src/api.ts create mode 100644 7project/frontend/src/pages/Dashboard.tsx create mode 100644 7project/frontend/src/pages/LoginRegisterPage.tsx diff --git a/7project/frontend/src/App.tsx b/7project/frontend/src/App.tsx index b6cce59..93626c2 100644 --- a/7project/frontend/src/App.tsx +++ b/7project/frontend/src/App.tsx @@ -1,39 +1,27 @@ -import { useState } from 'react' -import reactLogo from './assets/react.svg' -import viteLogo from '/vite.svg' -import './App.css' -import { BACKEND_URL } from './config' +import { useEffect, useState } from 'react'; +import './App.css'; +import LoginRegisterPage from './pages/LoginRegisterPage'; +import Dashboard from './pages/Dashboard'; +import { logout } from './api'; function App() { - const [count, setCount] = useState(0) + const [hasToken, setHasToken] = useState(!!localStorage.getItem('token')); + + useEffect(() => { + const onStorage = (e: StorageEvent) => { + if (e.key === 'token') setHasToken(!!e.newValue); + }; + window.addEventListener('storage', onStorage); + return () => window.removeEventListener('storage', onStorage); + }, []); + + if (!hasToken) { + return setHasToken(true)} />; + } return ( - <> -
- - Vite logo - - - React logo - -
-

Vite + React

-
- -

- Edit src/App.tsx and save to test HMR -

-

- Backend URL: {BACKEND_URL || '(not configured)'} -

-
-

- Click on the Vite and React logos to learn more -

- - ) + { logout(); setHasToken(false); }} /> + ); } -export default App +export default App; diff --git a/7project/frontend/src/api.ts b/7project/frontend/src/api.ts new file mode 100644 index 0000000..ff2d679 --- /dev/null +++ b/7project/frontend/src/api.ts @@ -0,0 +1,100 @@ +import { BACKEND_URL } from './config'; + +export type LoginResponse = { + access_token: string; + token_type: string; +}; + +export type Category = { + id: number; + name: string; + description?: string | null; +}; + +export type Transaction = { + id: number; + amount: number; + description?: string | null; + category_ids: number[]; +}; + +function getBaseUrl() { + const base = BACKEND_URL?.replace(/\/$/, '') || ''; + return base || ''; +} + +function authHeaders() { + const token = localStorage.getItem('token'); + return token ? { Authorization: `Bearer ${token}` } : {}; +} + +export async function login(email: string, password: string): Promise { + const body = new URLSearchParams(); + body.set('username', email); + body.set('password', password); + + const res = await fetch(`${getBaseUrl()}/auth/jwt/login`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: body.toString(), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || 'Login failed'); + } + const data: LoginResponse = await res.json(); + localStorage.setItem('token', data.access_token); +} + +export async function register(email: string, password: string, first_name?: string, last_name?: string): Promise { + const res = await fetch(`${getBaseUrl()}/auth/register`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ email, password, first_name, last_name }), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || 'Registration failed'); + } +} + +export async function getCategories(): Promise { + const res = await fetch(`${getBaseUrl()}/categories/`, { + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + }); + if (!res.ok) throw new Error('Failed to load categories'); + return res.json(); +} + +export type CreateTransactionInput = { + amount: number; + description?: string; + category_ids?: number[]; +}; + +export async function createTransaction(input: CreateTransactionInput): Promise { + const res = await fetch(`${getBaseUrl()}/transactions/create`, { + method: 'POST', + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + body: JSON.stringify(input), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || 'Failed to create transaction'); + } + return res.json(); +} + +export async function getTransactions(): Promise { + const res = await fetch(`${getBaseUrl()}/transactions/`, { + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + }); + if (!res.ok) throw new Error('Failed to load transactions'); + return res.json(); +} + +export function logout() { + localStorage.removeItem('token'); +} diff --git a/7project/frontend/src/pages/Dashboard.tsx b/7project/frontend/src/pages/Dashboard.tsx new file mode 100644 index 0000000..f606fdb --- /dev/null +++ b/7project/frontend/src/pages/Dashboard.tsx @@ -0,0 +1,155 @@ +import { useEffect, useMemo, useState } from 'react'; +import { type Category, type Transaction, createTransaction, getCategories, getTransactions } from '../api'; + +function formatAmount(n: number) { + return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n); +} + +export default function Dashboard({ onLogout }: { onLogout: () => void }) { + const [transactions, setTransactions] = useState([]); + const [categories, setCategories] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + // New transaction form state + const [amount, setAmount] = useState(''); + const [description, setDescription] = useState(''); + const [selectedCategoryId, setSelectedCategoryId] = useState(''); + + // Filters + const [minAmount, setMinAmount] = useState(''); + const [maxAmount, setMaxAmount] = useState(''); + const [filterCategoryId, setFilterCategoryId] = useState(''); + const [searchText, setSearchText] = useState(''); + + async function loadAll() { + setLoading(true); + setError(null); + try { + const [txs, cats] = await Promise.all([getTransactions(), getCategories()]); + setTransactions(txs); + setCategories(cats); + } catch (err: any) { + setError(err?.message || 'Failed to load data'); + } finally { + setLoading(false); + } + } + + useEffect(() => { + loadAll(); + }, []); + + const last10 = useMemo(() => { + const sorted = [...transactions].sort((a, b) => b.id - a.id); + return sorted.slice(0, 10); + }, [transactions]); + + const filtered = useMemo(() => { + let arr = last10; + const min = minAmount !== '' ? Number(minAmount) : undefined; + const max = maxAmount !== '' ? Number(maxAmount) : undefined; + if (min !== undefined) arr = arr.filter(t => t.amount >= min); + if (max !== undefined) arr = arr.filter(t => t.amount <= max); + if (filterCategoryId !== '') arr = arr.filter(t => t.category_ids.includes(filterCategoryId as number)); + if (searchText.trim()) arr = arr.filter(t => (t.description || '').toLowerCase().includes(searchText.toLowerCase())); + return arr; + }, [last10, minAmount, maxAmount, filterCategoryId, searchText]); + + function categoryNameById(id: number) { + return categories.find(c => c.id === id)?.name || `#${id}`; + } + + async function handleCreate(e: React.FormEvent) { + e.preventDefault(); + if (!amount) return; + const payload = { + amount: Number(amount), + description: description || undefined, + category_ids: selectedCategoryId !== '' ? [Number(selectedCategoryId)] : undefined, + }; + try { + const created = await createTransaction(payload); + setTransactions(prev => [created, ...prev]); + // reset form + setAmount(''); + setDescription(''); + setSelectedCategoryId(''); + } catch (err: any) { + alert(err?.message || 'Failed to create transaction'); + } + } + + return ( +
+
+

Dashboard

+ +
+ +
+

Add Transaction

+
+ setAmount(e.target.value)} required /> + setDescription(e.target.value)} /> + + +
+
+ +
+

Filters

+
+ setMinAmount(e.target.value)} /> + setMaxAmount(e.target.value)} /> + + setSearchText(e.target.value)} /> +
+
+ +
+

Latest Transactions (last 10)

+ {loading ? ( +
Loading…
+ ) : error ? ( +
{error}
+ ) : filtered.length === 0 ? ( +
No transactions
+ ) : ( + + + + + + + + + + + {filtered.map(t => ( + + + + + + + ))} + +
IDAmountDescriptionCategories
{t.id}{formatAmount(t.amount)}{t.description || ''} + {t.category_ids.map(id => categoryNameById(id)).join(', ')} +
+ )} +
+
+ ); +} diff --git a/7project/frontend/src/pages/LoginRegisterPage.tsx b/7project/frontend/src/pages/LoginRegisterPage.tsx new file mode 100644 index 0000000..e265167 --- /dev/null +++ b/7project/frontend/src/pages/LoginRegisterPage.tsx @@ -0,0 +1,71 @@ +import { useState } from 'react'; +import { login, register } from '../api'; + +export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => void }) { + const [mode, setMode] = useState<'login' | 'register'>('login'); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [firstName, setFirstName] = useState(''); + const [lastName, setLastName] = useState(''); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + async function handleSubmit(e: React.FormEvent) { + e.preventDefault(); + setLoading(true); + setError(null); + try { + if (mode === 'login') { + await login(email, password); + onLoggedIn(); + } else { + await register(email, password, firstName || undefined, lastName || undefined); + // After register, prompt login automatically + await login(email, password); + onLoggedIn(); + } + } catch (err: any) { + setError(err?.message || 'Operation failed'); + } finally { + setLoading(false); + } + } + + return ( +
+

{mode === 'login' ? 'Login' : 'Register'}

+
+ + +
+
+
+ +
+
+ +
+ {mode === 'register' && ( + <> +
+ +
+
+ +
+ + )} + {error &&
{error}
} + +
+
+ ); +} From eb087e457c3b2c1dfafa9cd5a79b90a20e74f843 Mon Sep 17 00:00:00 2001 From: ribardej Date: Wed, 15 Oct 2025 10:06:22 +0200 Subject: [PATCH 2/4] feat(frontend): improved and centered UI --- 7project/frontend/src/App.css | 43 +---- 7project/frontend/src/api.ts | 2 +- 7project/frontend/src/main.tsx | 1 + 7project/frontend/src/pages/Dashboard.tsx | 155 +++++++++--------- .../frontend/src/pages/LoginRegisterPage.tsx | 78 +++++---- 5 files changed, 127 insertions(+), 152 deletions(-) diff --git a/7project/frontend/src/App.css b/7project/frontend/src/App.css index b9d355d..c776d1c 100644 --- a/7project/frontend/src/App.css +++ b/7project/frontend/src/App.css @@ -1,42 +1 @@ -#root { - max-width: 1280px; - margin: 0 auto; - padding: 2rem; - text-align: center; -} - -.logo { - height: 6em; - padding: 1.5em; - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #646cffaa); -} -.logo.react:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -@keyframes logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} - -@media (prefers-reduced-motion: no-preference) { - a:nth-of-type(2) .logo { - animation: logo-spin infinite 20s linear; - } -} - -.card { - padding: 2em; -} - -.read-the-docs { - color: #888; -} +/* App-level styles moved to ui.css for a cleaner layout. */ diff --git a/7project/frontend/src/api.ts b/7project/frontend/src/api.ts index ff2d679..95933d4 100644 --- a/7project/frontend/src/api.ts +++ b/7project/frontend/src/api.ts @@ -88,7 +88,7 @@ export async function createTransaction(input: CreateTransactionInput): Promise< } export async function getTransactions(): Promise { - const res = await fetch(`${getBaseUrl()}/transactions/`, { + const res = await fetch(`${getBaseUrl()}/transactions/`, { headers: { 'Content-Type': 'application/json', ...authHeaders() }, }); if (!res.ok) throw new Error('Failed to load transactions'); diff --git a/7project/frontend/src/main.tsx b/7project/frontend/src/main.tsx index bef5202..d8e0921 100644 --- a/7project/frontend/src/main.tsx +++ b/7project/frontend/src/main.tsx @@ -1,6 +1,7 @@ import { StrictMode } from 'react' import { createRoot } from 'react-dom/client' import './index.css' +import './ui.css' import App from './App.tsx' createRoot(document.getElementById('root')!).render( diff --git a/7project/frontend/src/pages/Dashboard.tsx b/7project/frontend/src/pages/Dashboard.tsx index f606fdb..2c6b05f 100644 --- a/7project/frontend/src/pages/Dashboard.tsx +++ b/7project/frontend/src/pages/Dashboard.tsx @@ -36,9 +36,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) { } } - useEffect(() => { - loadAll(); - }, []); + useEffect(() => { loadAll(); }, []); const last10 = useMemo(() => { const sorted = [...transactions].sort((a, b) => b.id - a.id); @@ -56,9 +54,7 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) { return arr; }, [last10, minAmount, maxAmount, filterCategoryId, searchText]); - function categoryNameById(id: number) { - return categories.find(c => c.id === id)?.name || `#${id}`; - } + function categoryNameById(id: number) { return categories.find(c => c.id === id)?.name || `#${id}`; } async function handleCreate(e: React.FormEvent) { e.preventDefault(); @@ -71,85 +67,90 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) { try { const created = await createTransaction(payload); setTransactions(prev => [created, ...prev]); - // reset form - setAmount(''); - setDescription(''); - setSelectedCategoryId(''); + setAmount(''); setDescription(''); setSelectedCategoryId(''); } catch (err: any) { alert(err?.message || 'Failed to create transaction'); } } return ( -
-
-

Dashboard

- -
- -
-

Add Transaction

-
- setAmount(e.target.value)} required /> - setDescription(e.target.value)} /> - - -
-
- -
-

Filters

-
- setMinAmount(e.target.value)} /> - setMaxAmount(e.target.value)} /> - - setSearchText(e.target.value)} /> +
+ +
+
+

Dashboard

+
+ Signed in + +
-
+
+
+

Add Transaction

+
+ setAmount(e.target.value)} required /> + setDescription(e.target.value)} /> + + +
+
-
-

Latest Transactions (last 10)

- {loading ? ( -
Loading…
- ) : error ? ( -
{error}
- ) : filtered.length === 0 ? ( -
No transactions
- ) : ( - - - - - - - - - - - {filtered.map(t => ( - - - - - - - ))} - -
IDAmountDescriptionCategories
{t.id}{formatAmount(t.amount)}{t.description || ''} - {t.category_ids.map(id => categoryNameById(id)).join(', ')} -
- )} -
+
+

Filters

+
+ setMinAmount(e.target.value)} /> + setMaxAmount(e.target.value)} /> + + setSearchText(e.target.value)} /> +
+
+ +
+

Latest Transactions (last 10)

+ {loading ? ( +
Loading…
+ ) : error ? ( +
{error}
+ ) : filtered.length === 0 ? ( +
No transactions
+ ) : ( + + + + + + + + + + + {filtered.map(t => ( + + + + + + + ))} + +
IDAmountDescriptionCategories
{t.id}{formatAmount(t.amount)}{t.description || ''}{t.category_ids.map(id => categoryNameById(id)).join(', ')}
+ )} +
+
+
); } diff --git a/7project/frontend/src/pages/LoginRegisterPage.tsx b/7project/frontend/src/pages/LoginRegisterPage.tsx index e265167..ce333d0 100644 --- a/7project/frontend/src/pages/LoginRegisterPage.tsx +++ b/7project/frontend/src/pages/LoginRegisterPage.tsx @@ -1,6 +1,7 @@ -import { useState } from 'react'; +import { useState, useEffect } from 'react'; import { login, register } from '../api'; + export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => void }) { const [mode, setMode] = useState<'login' | 'register'>('login'); const [email, setEmail] = useState(''); @@ -31,41 +32,54 @@ export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => vo } } + // Add this useEffect hook + useEffect(() => { + // When the component mounts, add a class to the body + document.body.classList.add('auth-page'); + + // When the component unmounts, remove the class + return () => { + document.body.classList.remove('auth-page'); + }; + }, []); // The empty array ensures this runs only once + + // The JSX no longer needs the wrapper div return ( -
-

{mode === 'login' ? 'Login' : 'Register'}

-
- - +
+
+

{mode === 'login' ? 'Welcome back' : 'Create your account'}

+
+ + +
-
-
- -
-
- -
- {mode === 'register' && ( - <> -
- + +
+ + setEmail(e.target.value)} /> +
+
+ + setPassword(e.target.value)} /> +
+ {mode === 'register' && ( +
+
+ + setFirstName(e.target.value)} /> +
+
+ + setLastName(e.target.value)} /> +
-
- -
- - )} - {error &&
{error}
} - + )} + {error &&
{error}
} +
+ +
); } + From f208e739861ec35b912ef358169689329bd68e6c Mon Sep 17 00:00:00 2001 From: ribardej Date: Wed, 15 Oct 2025 11:00:47 +0200 Subject: [PATCH 3/4] feat(frontend): added account and appearance tabs --- 7project/frontend/src/App.tsx | 12 ++ 7project/frontend/src/api.ts | 43 ++++++ 7project/frontend/src/main.tsx | 3 + 7project/frontend/src/pages/Dashboard.tsx | 134 ++++++++++-------- .../frontend/src/pages/LoginRegisterPage.tsx | 15 +- 5 files changed, 146 insertions(+), 61 deletions(-) diff --git a/7project/frontend/src/App.tsx b/7project/frontend/src/App.tsx index 93626c2..3f3cec6 100644 --- a/7project/frontend/src/App.tsx +++ b/7project/frontend/src/App.tsx @@ -8,6 +8,18 @@ function App() { const [hasToken, setHasToken] = useState(!!localStorage.getItem('token')); useEffect(() => { + // Handle OAuth callback: /oauth-callback?access_token=...&token_type=... + if (window.location.pathname === '/oauth-callback') { + const params = new URLSearchParams(window.location.search); + const token = params.get('access_token'); + if (token) { + localStorage.setItem('token', token); + setHasToken(true); + } + // Clean URL and redirect to home + window.history.replaceState({}, '', '/'); + } + const onStorage = (e: StorageEvent) => { if (e.key === 'token') setHasToken(!!e.newValue); }; diff --git a/7project/frontend/src/api.ts b/7project/frontend/src/api.ts index 95933d4..2b7a2cb 100644 --- a/7project/frontend/src/api.ts +++ b/7project/frontend/src/api.ts @@ -95,6 +95,49 @@ export async function getTransactions(): Promise { return res.json(); } +export type User = { + id: string; + email: string; + first_name?: string | null; + last_name?: string | null; + is_active: boolean; + is_superuser: boolean; + is_verified: boolean; +}; + +export async function getMe(): Promise { + const res = await fetch(`${getBaseUrl()}/users/me`, { + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + }); + if (!res.ok) throw new Error('Failed to load user'); + return res.json(); +} + +export type UpdateMeInput = Partial> & { password?: string }; +export async function updateMe(input: UpdateMeInput): Promise { + const res = await fetch(`${getBaseUrl()}/users/me`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json', ...authHeaders() }, + body: JSON.stringify(input), + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || 'Failed to update user'); + } + return res.json(); +} + +export async function deleteMe(): Promise { + const res = await fetch(`${getBaseUrl()}/users/me`, { + method: 'DELETE', + headers: { ...authHeaders() }, + }); + if (!res.ok) { + const text = await res.text(); + throw new Error(text || 'Failed to delete account'); + } +} + export function logout() { localStorage.removeItem('token'); } diff --git a/7project/frontend/src/main.tsx b/7project/frontend/src/main.tsx index d8e0921..9d33e35 100644 --- a/7project/frontend/src/main.tsx +++ b/7project/frontend/src/main.tsx @@ -3,6 +3,9 @@ import { createRoot } from 'react-dom/client' import './index.css' import './ui.css' import App from './App.tsx' +import { applyAppearanceFromStorage } from './appearance' + +applyAppearanceFromStorage() createRoot(document.getElementById('root')!).render( diff --git a/7project/frontend/src/pages/Dashboard.tsx b/7project/frontend/src/pages/Dashboard.tsx index 2c6b05f..4f78f76 100644 --- a/7project/frontend/src/pages/Dashboard.tsx +++ b/7project/frontend/src/pages/Dashboard.tsx @@ -1,11 +1,14 @@ import { useEffect, useMemo, useState } from 'react'; import { type Category, type Transaction, createTransaction, getCategories, getTransactions } from '../api'; +import AccountPage from './AccountPage'; +import AppearancePage from './AppearancePage'; function formatAmount(n: number) { return new Intl.NumberFormat(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(n); } export default function Dashboard({ onLogout }: { onLogout: () => void }) { + const [current, setCurrent] = useState<'home' | 'account' | 'appearance'>('home'); const [transactions, setTransactions] = useState([]); const [categories, setCategories] = useState([]); const [loading, setLoading] = useState(true); @@ -78,77 +81,90 @@ export default function Dashboard({ onLogout }: { onLogout: () => void }) {
-

Dashboard

+

{current === 'home' ? 'Dashboard' : current === 'account' ? 'Account' : 'Appearance'}

Signed in
-
-

Add Transaction

-
- setAmount(e.target.value)} required /> - setDescription(e.target.value)} /> - - -
-
+ {current === 'home' && ( + <> +
+

Add Transaction

+
+ setAmount(e.target.value)} required /> + setDescription(e.target.value)} /> + + +
+
-
-

Filters

-
- setMinAmount(e.target.value)} /> - setMaxAmount(e.target.value)} /> - - setSearchText(e.target.value)} /> -
-
+
+

Filters

+
+ setMinAmount(e.target.value)} /> + setMaxAmount(e.target.value)} /> + + setSearchText(e.target.value)} /> +
+
-
-

Latest Transactions (last 10)

- {loading ? ( -
Loading…
- ) : error ? ( -
{error}
- ) : filtered.length === 0 ? ( -
No transactions
- ) : ( - - - - - - - - - - - {filtered.map(t => ( - - - - - - - ))} - -
IDAmountDescriptionCategories
{t.id}{formatAmount(t.amount)}{t.description || ''}{t.category_ids.map(id => categoryNameById(id)).join(', ')}
- )} -
+
+

Latest Transactions (last 10)

+ {loading ? ( +
Loading…
+ ) : error ? ( +
{error}
+ ) : filtered.length === 0 ? ( +
No transactions
+ ) : ( + + + + + + + + + + + {filtered.map(t => ( + + + + + + + ))} + +
IDAmountDescriptionCategories
{t.id}{formatAmount(t.amount)}{t.description || ''}{t.category_ids.map(id => categoryNameById(id)).join(', ')}
+ )} +
+ + )} + + {current === 'account' && ( + // lazy import avoided for simplicity + + )} + + {current === 'appearance' && ( + + )}
diff --git a/7project/frontend/src/pages/LoginRegisterPage.tsx b/7project/frontend/src/pages/LoginRegisterPage.tsx index ce333d0..50896fe 100644 --- a/7project/frontend/src/pages/LoginRegisterPage.tsx +++ b/7project/frontend/src/pages/LoginRegisterPage.tsx @@ -1,6 +1,12 @@ import { useState, useEffect } from 'react'; import { login, register } from '../api'; +import { BACKEND_URL } from '../config'; +function oauthUrl(provider: 'mojeid' | 'bankid') { + const base = BACKEND_URL.replace(/\/$/, ''); + const redirect = encodeURIComponent(window.location.origin + '/oauth-callback'); + return `${base}/auth/${provider}/authorize?redirect_url=${redirect}`; +} export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => void }) { const [mode, setMode] = useState<'login' | 'register'>('login'); @@ -75,8 +81,13 @@ export default function LoginRegisterPage({ onLoggedIn }: { onLoggedIn: () => vo
)} {error &&
{error}
} -
- +
+
Or continue with
+
+ MojeID + BankID + +
From c21af2732eef14994bc0fdc9d65c7bb438fb939f Mon Sep 17 00:00:00 2001 From: ribardej Date: Wed, 15 Oct 2025 11:11:04 +0200 Subject: [PATCH 4/4] feat(backend): implemented self delete for users --- 7project/backend/app/api/auth.py | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/7project/backend/app/api/auth.py b/7project/backend/app/api/auth.py index 576e80e..075600c 100644 --- a/7project/backend/app/api/auth.py +++ b/7project/backend/app/api/auth.py @@ -1,10 +1,28 @@ -from fastapi import APIRouter +from fastapi import APIRouter, Depends, status +from fastapi_users import models +from fastapi_users.manager import BaseUserManager from app.schemas.user import UserCreate, UserRead, UserUpdate from app.services.user_service import auth_backend, fastapi_users router = APIRouter() +@router.delete( + "/users/me", + status_code=status.HTTP_204_NO_CONTENT, + tags=["users"], + summary="Delete current user", + response_description="The user has been successfully deleted.", +) +async def delete_me( + user: models.UserProtocol = Depends(fastapi_users.current_user(active=True)), + user_manager: BaseUserManager = Depends(fastapi_users.get_user_manager), +): + """ + Delete the currently authenticated user. + """ + await user_manager.delete(user) + # Keep existing paths as-is under /auth/* and /users/* router.include_router( fastapi_users.get_auth_router(auth_backend), prefix="/auth/jwt", tags=["auth"]