diff --git a/backend/app/settings/__init__.py b/backend/app/settings/__init__.py new file mode 100644 index 0000000..95e73ce --- /dev/null +++ b/backend/app/settings/__init__.py @@ -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 diff --git a/frontend/src/api/settings.js b/frontend/src/api/settings.js new file mode 100644 index 0000000..edf92ae --- /dev/null +++ b/frontend/src/api/settings.js @@ -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; + } +}; diff --git a/frontend/src/components/settings/general/GeneralContainer.jsx b/frontend/src/components/settings/general/GeneralContainer.jsx index d851c1b..86d1449 100644 --- a/frontend/src/components/settings/general/GeneralContainer.jsx +++ b/frontend/src/components/settings/general/GeneralContainer.jsx @@ -1,6 +1,13 @@ // settings/general/GeneralContainer.jsx import React, {useState, useEffect} from '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 [loading, setLoading] = useState(true); @@ -28,16 +35,17 @@ const GeneralContainer = () => { const fetchSettings = async () => { setLoading(true); try { - console.log('Fetching general settings'); - setFormData({ - apiKey: 'sk-1234567890abcdef', - username: 'admin', + const {username, api_key} = await fetchGeneralSettings(); + setFormData(prev => ({ + ...prev, + apiKey: api_key, + username: username, + currentUsername: username, usernameCurrentPassword: '', password: '', confirmPassword: '', - currentPassword: '', - currentUsername: 'admin' - }); + currentPassword: '' + })); } catch (error) { console.error('Error fetching settings:', error); } finally { @@ -51,12 +59,21 @@ const GeneralContainer = () => { setTimeout(() => setCopySuccess(false), 1000); }; - const handleResetApiKey = () => { + const handleResetApiKey = async () => { 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.' ); + 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 = () => { - console.log('Will save username:', { - newUsername: formData.username, - currentPassword: formData.usernameCurrentPassword - }); - setFormData(prev => ({ - ...prev, - currentUsername: formData.username, - usernameCurrentPassword: '' - })); + const handleSaveUsername = async () => { + try { + await updateUsername( + formData.username, + formData.usernameCurrentPassword + ); + setFormData(prev => ({ + ...prev, + currentUsername: formData.username, + usernameCurrentPassword: '' + })); + } catch (error) { + console.error('Error updating username:', error); + } }; - const handleSavePassword = () => { + const handleSavePassword = async () => { if (formData.password !== formData.confirmPassword) { - alert('Passwords do not match'); + Alert.error('Passwords do not match'); return; } + 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) { - console.log('Will save password with verification', { - newPassword: formData.password, - currentPassword: formData.currentPassword - }); + try { + const response = await updatePassword( + formData.currentPassword, + formData.password + ); + + if (response.requireRelogin) { + window.location.href = '/login'; + } + } catch (error) { + console.error('Error updating password:', error); + } } };