mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +01:00
feat: implement settings API integration for fetching and updating user settings
This commit is contained in:
126
backend/app/settings/__init__.py
Normal file
126
backend/app/settings/__init__.py
Normal 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
|
||||||
137
frontend/src/api/settings.js
Normal file
137
frontend/src/api/settings.js
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user