mirror of
https://github.com/johndoe6345789/metabuilder.git
synced 2026-04-24 13:54:57 +00:00
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:
@@ -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():
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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')
|
||||
|
||||
Reference in New Issue
Block a user