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) <noreply@anthropic.com>
This commit is contained in:
2026-03-18 21:09:42 +00:00
parent 196d818f1b
commit 323086471d
3 changed files with 69 additions and 36 deletions

View File

@@ -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():

View File

@@ -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 {

View File

@@ -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<string, string> {
@@ -39,17 +31,6 @@ function authHeaders(): Record<string, string> {
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<string, unknown>[] = 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')