feat: implement settings API integration for fetching and updating user settings

This commit is contained in:
Sam Chau
2025-02-02 09:08:27 +10:30
parent 3757cbdb21
commit ff575f5a2a
3 changed files with 320 additions and 26 deletions

View File

@@ -0,0 +1,126 @@
# backend/app/settings/__init__.py
from flask import Blueprint, jsonify, request, session
from werkzeug.security import generate_password_hash, check_password_hash
import secrets
from ..db import get_db
import logging
logger = logging.getLogger(__name__)
bp = Blueprint('settings', __name__, url_prefix='/settings')
@bp.route('/general', methods=['GET'])
def get_general_settings():
db = get_db()
try:
user = db.execute('SELECT username, api_key FROM auth').fetchone()
if not user:
logger.error('No user found in auth table')
return jsonify({'error': 'No user configuration found'}), 500
return jsonify({
'username': user['username'],
'api_key': user['api_key']
})
except Exception as e:
logger.error(f'Error fetching general settings: {str(e)}')
return jsonify({'error': 'Failed to fetch settings'}), 500
@bp.route('/username', methods=['PUT'])
def update_username():
db = get_db()
data = request.get_json()
new_username = data.get('username')
current_password = data.get('current_password')
if not new_username or not current_password:
return jsonify({'error':
'Username and current password are required'}), 400
try:
# Verify current password
user = db.execute('SELECT password_hash FROM auth').fetchone()
if not check_password_hash(user['password_hash'], current_password):
logger.warning('Failed username change - invalid password')
return jsonify({'error': 'Invalid password'}), 401
db.execute('UPDATE auth SET username = ?', (new_username, ))
db.commit()
logger.info(f'Username updated to: {new_username}')
return jsonify({'message': 'Username updated successfully'})
except Exception as e:
logger.error(f'Failed to update username: {str(e)}')
return jsonify({'error': 'Failed to update username'}), 500
@bp.route('/password', methods=['PUT'])
def update_password():
db = get_db()
data = request.get_json()
current_password = data.get('current_password')
new_password = data.get('new_password')
if not current_password or not new_password:
return jsonify({'error':
'Current and new passwords are required'}), 400
try:
# Verify current password
user = db.execute(
'SELECT password_hash, session_id FROM auth').fetchone()
if not check_password_hash(user['password_hash'], current_password):
logger.warning('Failed password change - invalid current password')
return jsonify({'error': 'Invalid current password'}), 401
# Update password and generate a new session ID
password_hash = generate_password_hash(new_password)
new_session_id = secrets.token_urlsafe(32)
db.execute('UPDATE auth SET password_hash = ?, session_id = ?',
(password_hash, new_session_id))
db.commit()
# Clear the current session to force re-login
session.clear()
logger.info('Password updated successfully')
return jsonify({
'message': 'Password updated successfully. Please log in again.',
'requireRelogin': True
})
except Exception as e:
logger.error(f'Failed to update password: {str(e)}')
return jsonify({'error': 'Failed to update password'}), 500
@bp.route('/api-key', methods=['POST'])
def reset_api_key():
db = get_db()
data = request.get_json()
current_password = data.get('current_password')
if not current_password:
return jsonify({'error': 'Current password is required'}), 400
try:
# Verify current password
user = db.execute('SELECT password_hash FROM auth').fetchone()
if not check_password_hash(user['password_hash'], current_password):
logger.warning('Failed API key reset - invalid password')
return jsonify({'error': 'Invalid password'}), 401
# Generate and save new API key
new_api_key = secrets.token_urlsafe(32)
db.execute('UPDATE auth SET api_key = ?', (new_api_key, ))
db.commit()
logger.info('API key reset successfully')
return jsonify({
'message': 'API key reset successfully',
'api_key': new_api_key
})
except Exception as e:
logger.error(f'Failed to reset API key: {str(e)}')
return jsonify({'error': 'Failed to reset API key'}), 500

View File

@@ -0,0 +1,137 @@
// @api/settings.js
import Alert from '@ui/Alert';
const API_PREFIX = '/api';
// Helper function to mark errors that have already been handled
const createHandledError = message => {
const error = new Error(message);
error.isHandled = true;
return error;
};
export const fetchSettings = async () => {
try {
const response = await fetch(`${API_PREFIX}/settings`, {
credentials: 'include'
});
const data = await response.json();
if (!response.ok) {
const errorMessage = data.error || 'Failed to fetch settings';
Alert.error(errorMessage);
throw createHandledError(errorMessage);
}
return data;
} catch (error) {
if (!error.isHandled) {
Alert.error(error.message || 'Failed to fetch settings');
}
throw error;
}
};
export const fetchGeneralSettings = async () => {
try {
const response = await fetch(`${API_PREFIX}/settings/general`, {
credentials: 'include'
});
const data = await response.json();
if (!response.ok) {
const errorMessage =
data.error || 'Failed to fetch general settings';
Alert.error(errorMessage);
throw createHandledError(errorMessage);
}
return data;
} catch (error) {
if (!error.isHandled) {
Alert.error(error.message || 'Failed to fetch general settings');
}
throw error;
}
};
export const updateUsername = async (username, currentPassword) => {
try {
const response = await fetch(`${API_PREFIX}/settings/username`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({username, current_password: currentPassword})
});
const data = await response.json();
if (!response.ok) {
const errorMessage = data.error || 'Failed to update username';
Alert.error(errorMessage);
throw createHandledError(errorMessage);
}
Alert.success('Username updated successfully');
return data;
} catch (error) {
if (!error.isHandled) {
Alert.error(error.message || 'Failed to update username');
}
throw error;
}
};
export const updatePassword = async (currentPassword, newPassword) => {
try {
const response = await fetch(`${API_PREFIX}/settings/password`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({
current_password: currentPassword,
new_password: newPassword
})
});
const data = await response.json();
if (!response.ok) {
const errorMessage = data.error || 'Failed to update password';
Alert.error(errorMessage);
throw createHandledError(errorMessage);
}
Alert.success('Password updated successfully');
return data;
} catch (error) {
if (!error.isHandled) {
Alert.error(error.message || 'Failed to update password');
}
throw error;
}
};
export const resetApiKey = async currentPassword => {
try {
const response = await fetch(`${API_PREFIX}/settings/api-key`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
credentials: 'include',
body: JSON.stringify({current_password: currentPassword})
});
const data = await response.json();
if (!response.ok) {
const errorMessage = data.error || 'Failed to reset API key';
Alert.error(errorMessage);
throw createHandledError(errorMessage);
}
Alert.success('API key reset successfully');
return data;
} catch (error) {
if (!error.isHandled) {
Alert.error(error.message || 'Failed to reset API key');
}
throw error;
}
};

View File

@@ -1,6 +1,13 @@
// settings/general/GeneralContainer.jsx // settings/general/GeneralContainer.jsx
import React, {useState, useEffect} from 'react'; import React, {useState, useEffect} from 'react';
import {Eye, EyeOff, Copy, RefreshCw, Check} from 'lucide-react'; import {Eye, EyeOff, Copy, RefreshCw, Check} from 'lucide-react';
import {
fetchGeneralSettings,
updateUsername,
updatePassword,
resetApiKey
} from '@api/settings';
import Alert from '@ui/Alert';
const GeneralContainer = () => { const GeneralContainer = () => {
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
@@ -28,16 +35,17 @@ const GeneralContainer = () => {
const fetchSettings = async () => { const fetchSettings = async () => {
setLoading(true); setLoading(true);
try { try {
console.log('Fetching general settings'); const {username, api_key} = await fetchGeneralSettings();
setFormData({ setFormData(prev => ({
apiKey: 'sk-1234567890abcdef', ...prev,
username: 'admin', apiKey: api_key,
username: username,
currentUsername: username,
usernameCurrentPassword: '', usernameCurrentPassword: '',
password: '', password: '',
confirmPassword: '', confirmPassword: '',
currentPassword: '', currentPassword: ''
currentUsername: 'admin' }));
});
} catch (error) { } catch (error) {
console.error('Error fetching settings:', error); console.error('Error fetching settings:', error);
} finally { } finally {
@@ -51,12 +59,21 @@ const GeneralContainer = () => {
setTimeout(() => setCopySuccess(false), 1000); setTimeout(() => setCopySuccess(false), 1000);
}; };
const handleResetApiKey = () => { const handleResetApiKey = async () => {
const confirmed = window.confirm( const confirmed = window.confirm(
'Are you sure you want to reset your API key? This action cannot be undone and your current key will stop working immediately.' 'Are you sure you want to reset your API key? This action cannot be undone and your current key will stop working immediately.'
); );
if (confirmed) { if (confirmed) {
console.log('Will reset API key'); try {
const response = await resetApiKey(formData.currentPassword);
setFormData(prev => ({
...prev,
apiKey: response.api_key
}));
} catch (error) {
console.error('Error resetting API key:', error);
}
} }
}; };
@@ -96,31 +113,45 @@ const GeneralContainer = () => {
})); }));
}; };
const handleSaveUsername = () => { const handleSaveUsername = async () => {
console.log('Will save username:', { try {
newUsername: formData.username, await updateUsername(
currentPassword: formData.usernameCurrentPassword formData.username,
}); formData.usernameCurrentPassword
setFormData(prev => ({ );
...prev, setFormData(prev => ({
currentUsername: formData.username, ...prev,
usernameCurrentPassword: '' currentUsername: formData.username,
})); usernameCurrentPassword: ''
}));
} catch (error) {
console.error('Error updating username:', error);
}
}; };
const handleSavePassword = () => { const handleSavePassword = async () => {
if (formData.password !== formData.confirmPassword) { if (formData.password !== formData.confirmPassword) {
alert('Passwords do not match'); Alert.error('Passwords do not match');
return; return;
} }
const confirmed = window.confirm( const confirmed = window.confirm(
'Are you sure you want to change your password?' 'Are you sure you want to change your password? You will need to log in again.'
); );
if (confirmed) { if (confirmed) {
console.log('Will save password with verification', { try {
newPassword: formData.password, const response = await updatePassword(
currentPassword: formData.currentPassword formData.currentPassword,
}); formData.password
);
if (response.requireRelogin) {
window.location.href = '/login';
}
} catch (error) {
console.error('Error updating password:', error);
}
} }
}; };