From 323086471d784dfb2cd75f8ee3022e717042836d Mon Sep 17 00:00:00 2001 From: johndoe6345789 Date: Wed, 18 Mar 2026 21:09:42 +0000 Subject: [PATCH] fix(pastebin): persist bio via Flask backend and load on settings page Bio updates were sent directly to DBAL which returned 403 (user JWT lacks admin privileges). Moved profile read/write through new Flask endpoints (GET/PUT /api/profile) that use the DBAL admin token server-side. Also fixed ProfileSettingsCard to fetch and populate the existing bio on mount instead of always starting with an empty string. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontends/pastebin/backend/app.py | 40 +++++++++++++++++ .../settings/ProfileSettingsCard.tsx | 21 +++++++-- .../src/store/slices/profilesSlice.ts | 44 +++++-------------- 3 files changed, 69 insertions(+), 36 deletions(-) diff --git a/frontends/pastebin/backend/app.py b/frontends/pastebin/backend/app.py index b80b35df2..08acbc1b1 100644 --- a/frontends/pastebin/backend/app.py +++ b/frontends/pastebin/backend/app.py @@ -862,6 +862,46 @@ def get_me(): return jsonify({'id': user['id'], 'username': user['username']}) +@app.route('/api/profile', methods=['GET']) +@auth_required +def get_profile(): + """Return a user profile from DBAL. ?username=X looks up by username, otherwise returns the authenticated user.""" + username = request.args.get('username') + if username: + r = dbal_request('GET', f'/{DBAL_TENANT_ID}/core/User?filter.username={username}&limit=1') + if r and r.ok: + body = r.json() + items = body.get('data', {}).get('data', []) + if items: + return jsonify(items[0]) + return jsonify({'error': 'Profile not found'}), 404 + r = dbal_request('GET', f'/{DBAL_TENANT_ID}/core/User/{g.user_id}') + if r and r.ok: + body = r.json() + data = body.get('data', body) + return jsonify(data) + return jsonify({'error': 'Profile not found'}), 404 + + +@app.route('/api/profile', methods=['PUT']) +@auth_required +def update_profile(): + """Update the authenticated user's profile (bio, etc.) via DBAL.""" + payload = request.get_json(force=True, silent=True) or {} + # Only allow safe fields to be updated by the user + allowed = {} + if 'bio' in payload: + allowed['bio'] = str(payload['bio'])[:2000] + if not allowed: + return jsonify({'error': 'No updatable fields provided'}), 400 + r = dbal_request('PUT', f'/{DBAL_TENANT_ID}/core/User/{g.user_id}', allowed) + if r and r.ok: + body = r.json() + data = body.get('data', body) + return jsonify(data) + return jsonify({'error': 'Failed to update profile'}), 500 + + @app.route('/api/auth/settings', methods=['GET']) @auth_required def get_user_settings(): diff --git a/frontends/pastebin/src/components/settings/ProfileSettingsCard.tsx b/frontends/pastebin/src/components/settings/ProfileSettingsCard.tsx index b7ac7663d..7c496fe6a 100644 --- a/frontends/pastebin/src/components/settings/ProfileSettingsCard.tsx +++ b/frontends/pastebin/src/components/settings/ProfileSettingsCard.tsx @@ -1,22 +1,37 @@ 'use client'; -import { useState } from 'react'; +import { useEffect, useState } from 'react'; import { Card, CardContent, CardHeader, Button, FormLabel, MaterialIcon } from '@metabuilder/components/fakemui'; import { useAppSelector, useAppDispatch } from '@/store/hooks'; -import { selectCurrentUser } from '@/store/selectors'; -import { updateMyProfile } from '@/store/slices/profilesSlice'; +import { selectCurrentUser, selectUserProfile } from '@/store/selectors'; +import { updateMyProfile, fetchUserProfile } from '@/store/slices/profilesSlice'; import { MarkdownRenderer } from '@/components/error/MarkdownRenderer'; import styles from './settings-card.module.scss'; import profileStyles from './profile-settings-card.module.scss'; export function ProfileSettingsCard() { const user = useAppSelector(selectCurrentUser); + const profile = useAppSelector(state => user ? selectUserProfile(state, user.username) : null); const dispatch = useAppDispatch(); const [bio, setBio] = useState(''); + const [bioLoaded, setBioLoaded] = useState(false); const [preview, setPreview] = useState(false); const [saving, setSaving] = useState(false); const [saved, setSaved] = useState(false); + // Fetch the full profile (including bio) on mount + useEffect(() => { + if (user?.username) dispatch(fetchUserProfile(user.username)); + }, [user?.username, dispatch]); + + // Populate bio from the fetched profile once + useEffect(() => { + if (profile?.bio !== undefined && !bioLoaded) { + setBio(profile.bio); + setBioLoaded(true); + } + }, [profile, bioLoaded]); + const handleSave = async () => { setSaving(true); try { diff --git a/frontends/pastebin/src/store/slices/profilesSlice.ts b/frontends/pastebin/src/store/slices/profilesSlice.ts index d17a4e7f6..e94c121f4 100644 --- a/frontends/pastebin/src/store/slices/profilesSlice.ts +++ b/frontends/pastebin/src/store/slices/profilesSlice.ts @@ -1,5 +1,4 @@ import { createAsyncThunk, createSlice, type Action } from '@reduxjs/toolkit' -import { getStorageConfig } from '@/lib/storage' import { getAuthToken } from '@/lib/authToken' // --------------------------------------------------------------------------- @@ -20,18 +19,11 @@ interface ProfilesState { } // --------------------------------------------------------------------------- -// DBAL helpers +// Helpers // --------------------------------------------------------------------------- -const DBAL_TENANT = 'pastebin' -const DBAL_PACKAGE = 'pastebin' - -function dbalBaseUrl(): string { - return (getStorageConfig().dbalUrl ?? '').replace(/\/$/, '') -} - -function entityUrl(entity: string): string { - return `${dbalBaseUrl()}/${DBAL_TENANT}/${DBAL_PACKAGE}/${entity}` +function apiBase(): string { + return (process.env.NEXT_PUBLIC_FLASK_BACKEND_URL ?? '').replace(/\/$/, '') || '/pastebin-api' } function authHeaders(): Record { @@ -39,17 +31,6 @@ function authHeaders(): Record { return token ? { Authorization: `Bearer ${token}` } : {} } -function getUserId(): string { - const token = getAuthToken() - if (!token) return '' - try { - const payload = JSON.parse(atob(token.split('.')[1])) - return payload.sub ?? '' - } catch { - return '' - } -} - function getUsername(): string { const token = getAuthToken() if (!token) return '' @@ -78,13 +59,13 @@ export const fetchUserProfile = createAsyncThunk( 'profiles/fetchUserProfile', async (username: string, { rejectWithValue }) => { try { - const url = `${entityUrl('User')}?filter.username=${encodeURIComponent(username)}&limit=1` - const res = await fetch(url, { headers: authHeaders() }) + const res = await fetch(`${apiBase()}/api/profile?username=${encodeURIComponent(username)}`, { + headers: authHeaders(), + }) if (!res.ok) return rejectWithValue(`Failed to fetch profile: ${res.statusText}`) - const json = await res.json() - const items: Record[] = json.data?.data ?? json.data ?? [] - if (items.length === 0) return null - return toUserProfile(items[0]) + const data = await res.json() + if (!data.id) return null + return toUserProfile(data) } catch { return rejectWithValue('Network error') } @@ -95,16 +76,13 @@ export const updateMyProfile = createAsyncThunk( 'profiles/updateMyProfile', async (bio: string, { rejectWithValue }) => { try { - const userId = getUserId() - if (!userId) return rejectWithValue('Not authenticated') - const res = await fetch(`${entityUrl('User')}/${userId}`, { + const res = await fetch(`${apiBase()}/api/profile`, { method: 'PUT', headers: { 'Content-Type': 'application/json', ...authHeaders() }, body: JSON.stringify({ bio }), }) if (!res.ok) return rejectWithValue(`Failed to update profile: ${res.statusText}`) - const json = await res.json() - const data = json.data ?? json + const data = await res.json() return toUserProfile(data) } catch { return rejectWithValue('Network error')