diff --git a/.github/workflows/deploy-demo.yml b/.github/workflows/deploy-demo.yml index 9f6ff19..f5d36cf 100644 --- a/.github/workflows/deploy-demo.yml +++ b/.github/workflows/deploy-demo.yml @@ -53,34 +53,43 @@ jobs: # Since you wanted the database redeployed on every deployment, # we run a PHP script to clean it, apply schema, and create users. - php -r " - require 'api/db.php'; - try { - \$pdo->query('SET FOREIGN_KEY_CHECKS = 0'); - \$tables = \$pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN); - foreach (\$tables as \$table) { - \$pdo->query('DROP TABLE IF EXISTS \`' . \$table . '\`'); - } - - \$sql = file_get_contents('schema.sql'); - // Strip statements that might cause permission errors on shared hosting - \$sql = preg_replace('/\bCREATE\s+DATABASE\b[^;]+;/i', '', \$sql); - \$sql = preg_replace('/\bUSE\b\s+[^;]+;/i', '', \$sql); - \$pdo->exec(\$sql); - - \$pdo->query('SET FOREIGN_KEY_CHECKS = 1'); - - \$demoHash = password_hash('demo123', PASSWORD_DEFAULT); - - \$stmt = \$pdo->prepare('INSERT INTO users (id, username, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)'); - \$stmt->execute([bin2hex(random_bytes(18)), 'demo', 'demo@example.com', \$demoHash, 0]); - - echo \"Database successfully cleaned, schema imported, and users created!\n\"; - } catch (Exception \$e) { - echo \"DB Error: \" . \$e->getMessage() . \"\n\"; - exit(1); - } - " + # We do this via HTTP to avoid CLI missing extensions like PDO. + cat << 'EOF' > deploy_reset.php + query('SET FOREIGN_KEY_CHECKS = 0'); + $tables = $pdo->query('SHOW TABLES')->fetchAll(PDO::FETCH_COLUMN); + foreach ($tables as $table) { + $pdo->query('DROP TABLE IF EXISTS `' . $table . '`'); + } + + $sql = file_get_contents('schema.sql'); + // Strip statements that might cause permission errors on shared hosting + $sql = preg_replace('/\bCREATE\s+DATABASE\b[^;]+;/i', '', $sql); + $sql = preg_replace('/\bUSE\b\s+[^;]+;/i', '', $sql); + $pdo->exec($sql); + + $pdo->query('SET FOREIGN_KEY_CHECKS = 1'); + + $demoHash = password_hash('demo123', PASSWORD_DEFAULT); + + $stmt = $pdo->prepare('INSERT INTO users (id, username, email, password_hash, is_admin) VALUES (?, ?, ?, ?, ?)'); + $stmt->execute([bin2hex(random_bytes(18)), 'demo', 'demo@example.com', $demoHash, 0]); + + echo "Database successfully cleaned, schema imported, and users created!\n"; + } catch (Exception $e) { + echo "DB Error: " . $e->getMessage() . "\n"; + http_response_code(500); + exit(1); + } + EOF + + # Execute via web request to ensure correct PHP extensions are loaded + curl -sSf https://nester-demo.philip.ps/backend/deploy_reset.php || { echo "Curl failed"; rm -f deploy_reset.php; exit 1; } + + # Clean up + rm -f deploy_reset.php # Fix permissions for the webserver (assuming the usual www-data or client user) # chown -R web21:client1 /home/philippsnesterdemo/web/nester-demo/ || true diff --git a/public/logo.af b/public/logo.af new file mode 100644 index 0000000..a1090ce Binary files /dev/null and b/public/logo.af differ diff --git a/src/contexts/AuthContext.jsx b/src/contexts/AuthContext.jsx index a82c093..3d8264d 100644 --- a/src/contexts/AuthContext.jsx +++ b/src/contexts/AuthContext.jsx @@ -33,6 +33,15 @@ export const AuthProvider = ({ children }) => { setUser(null); }, []); + useEffect(() => { + const handleUnauthorized = () => { + logout(); + }; + window.addEventListener('auth:unauthorized', handleUnauthorized); + return () => window.removeEventListener('auth:unauthorized', handleUnauthorized); + }, [logout]); + + // Wrapper around fetch that automatically logs the user out on 401 responses. // Use this for all authenticated API calls instead of raw fetch(). const fetchWithAuth = useCallback(async (url, options = {}) => { diff --git a/src/screens/UserManagementScreen.jsx b/src/screens/UserManagementScreen.jsx index 30cad02..d5ba51b 100644 --- a/src/screens/UserManagementScreen.jsx +++ b/src/screens/UserManagementScreen.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from 'react'; import { useAuth } from '../contexts/AuthContext'; -import { API_BASE_URL } from '../services/nestService'; +import { API_BASE_URL, apiFetch } from '../services/nestService'; import './AuthScreens.css'; export default function UserManagementScreen({ onClose }) { @@ -14,7 +14,7 @@ export default function UserManagementScreen({ onClose }) { const fetchUsers = async () => { try { - const response = await fetch(`${API_BASE_URL}/auth/get_users.php`, { + const response = await apiFetch(`${API_BASE_URL}/auth/get_users.php`, { headers: { 'Authorization': `Bearer ${token}` } }); const data = await response.json(); @@ -38,7 +38,7 @@ export default function UserManagementScreen({ onClose }) { setErrorMessage(''); try { - const response = await fetch(`${API_BASE_URL}/auth/create_user.php`, { + const response = await apiFetch(`${API_BASE_URL}/auth/create_user.php`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/services/authService.js b/src/services/authService.js index 6b9005c..c7f020e 100644 --- a/src/services/authService.js +++ b/src/services/authService.js @@ -1,9 +1,9 @@ -import { API_BASE_URL } from './nestService'; +import { API_BASE_URL, apiFetch } from './nestService'; // We need to export API_BASE_URL from nestService, so we'll do that shortly as well. export const login = async (email, password) => { - const response = await fetch(`${API_BASE_URL}/auth/login.php`, { + const response = await apiFetch(`${API_BASE_URL}/auth/login.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email, password }) @@ -14,7 +14,7 @@ export const login = async (email, password) => { }; export const forgotPassword = async (email) => { - const response = await fetch(`${API_BASE_URL}/auth/forgot_password.php`, { + const response = await apiFetch(`${API_BASE_URL}/auth/forgot_password.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ email }) @@ -25,7 +25,7 @@ export const forgotPassword = async (email) => { }; export const resetPassword = async (token, newPassword) => { - const response = await fetch(`${API_BASE_URL}/auth/reset_password.php`, { + const response = await apiFetch(`${API_BASE_URL}/auth/reset_password.php`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ token, new_password: newPassword }) @@ -36,7 +36,7 @@ export const resetPassword = async (token, newPassword) => { }; export const changePassword = async (token, currentPassword, newPassword) => { - const response = await fetch(`${API_BASE_URL}/auth/change_password.php`, { + const response = await apiFetch(`${API_BASE_URL}/auth/change_password.php`, { method: 'POST', headers: { 'Content-Type': 'application/json', diff --git a/src/services/nestService.js b/src/services/nestService.js index 3beccd4..790d5ef 100644 --- a/src/services/nestService.js +++ b/src/services/nestService.js @@ -1,6 +1,9 @@ // Dynamically resolve the API base URL from the current server's origin. // This ensures the app works on any server without build-time configuration. export const API_BASE_URL = (() => { + if (import.meta.env.VITE_API_BASE_URL) { + return import.meta.env.VITE_API_BASE_URL; + } const { protocol, host } = window.location; return `${protocol}//${host}/backend/api`; })(); @@ -14,9 +17,26 @@ const getHeaders = (token) => { return headers; }; +// Wrapper around fetch to handle 401 Unauthorized globally +export const apiFetch = async (url, options = {}) => { + const response = await fetch(url, options); + if (response.status === 401) { + try { + const clone = response.clone(); + const data = await clone.json(); + if (data.error !== 'Incorrect current password' && data.error !== 'Invalid credentials') { + window.dispatchEvent(new Event('auth:unauthorized')); + } + } catch (e) { + window.dispatchEvent(new Event('auth:unauthorized')); + } + } + return response; +}; + export const loadNests = async (token) => { try { - const response = await fetch(`${API_BASE_URL}/get_nests.php`); + const response = await apiFetch(`${API_BASE_URL}/get_nests.php`); if (!response.ok) throw new Error('Network response was not ok'); return await response.json(); } catch (e) { @@ -35,7 +55,7 @@ export const addNest = async (latlng, name, token) => { }; try { - const response = await fetch(`${API_BASE_URL}/create_nest.php`, { + const response = await apiFetch(`${API_BASE_URL}/create_nest.php`, { method: 'POST', headers: getHeaders(token), body: JSON.stringify(newNest) @@ -59,7 +79,7 @@ export const addLogToNest = async (nestId, actionText, token) => { }; try { - const response = await fetch(`${API_BASE_URL}/add_log.php`, { + const response = await apiFetch(`${API_BASE_URL}/add_log.php`, { method: 'POST', headers: getHeaders(token), body: JSON.stringify(newLog) @@ -74,7 +94,7 @@ export const addLogToNest = async (nestId, actionText, token) => { export const deleteLogFromNest = async (logId, token) => { try { - const response = await fetch(`${API_BASE_URL}/delete_log.php`, { + const response = await apiFetch(`${API_BASE_URL}/delete_log.php`, { method: 'POST', headers: getHeaders(token), body: JSON.stringify({ id: logId }) @@ -103,7 +123,7 @@ export const uploadNestPhoto = async (nestId, photoFile, token) => { // fetch will automatically set it to multipart/form-data with the correct boundary when body is FormData. try { - const response = await fetch(`${API_BASE_URL}/upload_nest_photo.php`, { + const response = await apiFetch(`${API_BASE_URL}/upload_nest_photo.php`, { method: 'POST', headers: headers, body: formData @@ -118,7 +138,7 @@ export const uploadNestPhoto = async (nestId, photoFile, token) => { export const deleteNest = async (nestId, token) => { try { - await fetch(`${API_BASE_URL}/delete_nest.php`, { + await apiFetch(`${API_BASE_URL}/delete_nest.php`, { method: 'POST', headers: getHeaders(token), body: JSON.stringify({ id: nestId }) @@ -130,7 +150,7 @@ export const deleteNest = async (nestId, token) => { export const deleteNestPhoto = async (nestId, token) => { try { - const response = await fetch(`${API_BASE_URL}/delete_nest_photo.php`, { + const response = await apiFetch(`${API_BASE_URL}/delete_nest_photo.php`, { method: 'POST', headers: getHeaders(token), body: JSON.stringify({ id: nestId }) @@ -145,7 +165,7 @@ export const deleteNestPhoto = async (nestId, token) => { export const updateNestLocation = async (nestId, latlng, token) => { try { - await fetch(`${API_BASE_URL}/update_nest.php`, { + await apiFetch(`${API_BASE_URL}/update_nest.php`, { method: 'POST', headers: getHeaders(token), body: JSON.stringify({ id: nestId, lat: latlng.lat, lng: latlng.lng }) @@ -159,7 +179,7 @@ export const updateNestLocation = async (nestId, latlng, token) => { export const updateNestName = async (nestId, name, token) => { try { - const response = await fetch(`${API_BASE_URL}/update_nest_name.php`, { + const response = await apiFetch(`${API_BASE_URL}/update_nest_name.php`, { method: 'POST', headers: getHeaders(token), body: JSON.stringify({ id: nestId, name })