From 581dc5884c8babb98ea0d5c39a26b7f1099e77e2 Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Sun, 5 Oct 2025 19:12:51 +0100 Subject: [PATCH 1/2] Fixed issue with users not being updated Re-worked setup.sh to use last 3 tags and the main branch (development latest) --- backend/src/routes/authRoutes.js | 25 ++++- frontend/src/components/settings/UsersTab.jsx | 87 ++++++++++++++- frontend/src/pages/Profile.jsx | 14 ++- setup.sh | 104 +++++++++--------- 4 files changed, 172 insertions(+), 58 deletions(-) diff --git a/backend/src/routes/authRoutes.js b/backend/src/routes/authRoutes.js index 6f68209..7d66856 100644 --- a/backend/src/routes/authRoutes.js +++ b/backend/src/routes/authRoutes.js @@ -176,6 +176,8 @@ router.get( id: true, username: true, email: true, + first_name: true, + last_name: true, role: true, is_active: true, last_login: true, @@ -314,6 +316,14 @@ router.put( .isLength({ min: 3 }) .withMessage("Username must be at least 3 characters"), body("email").optional().isEmail().withMessage("Valid email is required"), + body("first_name") + .optional() + .isLength({ min: 1 }) + .withMessage("First name must be at least 1 character"), + body("last_name") + .optional() + .isLength({ min: 1 }) + .withMessage("Last name must be at least 1 character"), body("role") .optional() .custom(async (value) => { @@ -326,10 +336,10 @@ router.put( } return true; }), - body("isActive") + body("is_active") .optional() .isBoolean() - .withMessage("isActive must be a boolean"), + .withMessage("is_active must be a boolean"), ], async (req, res) => { try { @@ -340,13 +350,16 @@ router.put( return res.status(400).json({ errors: errors.array() }); } - const { username, email, role, isActive } = req.body; + const { username, email, first_name, last_name, role, is_active } = + req.body; const updateData = {}; if (username) updateData.username = username; if (email) updateData.email = email; + if (first_name !== undefined) updateData.first_name = first_name || null; + if (last_name !== undefined) updateData.last_name = last_name || null; if (role) updateData.role = role; - if (typeof isActive === "boolean") updateData.is_active = isActive; + if (typeof is_active === "boolean") updateData.is_active = is_active; // Check if user exists const existingUser = await prisma.users.findUnique({ @@ -381,7 +394,7 @@ router.put( } // Prevent deactivating the last admin - if (isActive === false && existingUser.role === "admin") { + if (is_active === false && existingUser.role === "admin") { const adminCount = await prisma.users.count({ where: { role: "admin", @@ -404,6 +417,8 @@ router.put( id: true, username: true, email: true, + first_name: true, + last_name: true, role: true, is_active: true, last_login: true, diff --git a/frontend/src/components/settings/UsersTab.jsx b/frontend/src/components/settings/UsersTab.jsx index d606452..d55d11c 100644 --- a/frontend/src/components/settings/UsersTab.jsx +++ b/frontend/src/components/settings/UsersTab.jsx @@ -92,7 +92,12 @@ const UsersTab = () => { }; const handleEditUser = (user) => { - setEditingUser(user); + // Reset editingUser first to force re-render with fresh data + setEditingUser(null); + // Use setTimeout to ensure the modal re-initializes with fresh data + setTimeout(() => { + setEditingUser(user); + }, 0); }; const handleResetPassword = (user) => { @@ -314,7 +319,9 @@ const UsersTab = () => { user={editingUser} isOpen={!!editingUser} onClose={() => setEditingUser(null)} - onUserUpdated={() => updateUserMutation.mutate()} + onUserUpdated={() => { + queryClient.invalidateQueries(["users"]); + }} roles={roles} /> )} @@ -352,11 +359,29 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => { }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + + // Reset form when modal is closed + useEffect(() => { + if (!isOpen) { + setFormData({ + username: "", + email: "", + password: "", + first_name: "", + last_name: "", + role: "user", + }); + setError(""); + setSuccess(false); + } + }, [isOpen]); const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(""); + setSuccess(false); try { // Only send role if roles are available from API @@ -364,12 +389,19 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => { username: formData.username, email: formData.email, password: formData.password, + first_name: formData.first_name, + last_name: formData.last_name, }; if (roles && Array.isArray(roles) && roles.length > 0) { payload.role = formData.role; } await adminUsersAPI.create(payload); + setSuccess(true); onUserCreated(); + // Auto-close after 1.5 seconds + setTimeout(() => { + onClose(); + }, 1500); } catch (err) { setError(err.response?.data?.error || "Failed to create user"); } finally { @@ -517,6 +549,17 @@ const AddUserModal = ({ isOpen, onClose, onUserCreated, roles }) => { + {success && ( +
+
+ +

+ User created successfully! +

+
+
+ )} + {error && (

@@ -566,15 +609,44 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => { }); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(""); + const [success, setSuccess] = useState(false); + + // Update formData when user prop changes or modal opens + useEffect(() => { + if (user && isOpen) { + setFormData({ + username: user.username || "", + email: user.email || "", + first_name: user.first_name || "", + last_name: user.last_name || "", + role: user.role || "user", + is_active: user.is_active ?? true, + }); + } + }, [user, isOpen]); + + // Reset error and success when modal closes + useEffect(() => { + if (!isOpen) { + setError(""); + setSuccess(false); + } + }, [isOpen]); const handleSubmit = async (e) => { e.preventDefault(); setIsLoading(true); setError(""); + setSuccess(false); try { await adminUsersAPI.update(user.id, formData); + setSuccess(true); onUserUpdated(); + // Auto-close after 1.5 seconds + setTimeout(() => { + onClose(); + }, 1500); } catch (err) { setError(err.response?.data?.error || "Failed to update user"); } finally { @@ -718,6 +790,17 @@ const EditUserModal = ({ user, isOpen, onClose, onUserUpdated, roles }) => {

+ {success && ( +
+
+ +

+ User updated successfully! +

+
+
+ )} + {error && (

diff --git a/frontend/src/pages/Profile.jsx b/frontend/src/pages/Profile.jsx index 5fc7d68..ce22b52 100644 --- a/frontend/src/pages/Profile.jsx +++ b/frontend/src/pages/Profile.jsx @@ -18,7 +18,7 @@ import { User, } from "lucide-react"; -import { useId, useState } from "react"; +import { useEffect, useId, useState } from "react"; import { useAuth } from "../contexts/AuthContext"; import { useTheme } from "../contexts/ThemeContext"; @@ -45,6 +45,18 @@ const Profile = () => { last_name: user?.last_name || "", }); + // Update profileData when user data changes + useEffect(() => { + if (user) { + setProfileData({ + username: user.username || "", + email: user.email || "", + first_name: user.first_name || "", + last_name: user.last_name || "", + }); + } + }, [user]); + const [passwordData, setPasswordData] = useState({ currentPassword: "", newPassword: "", diff --git a/setup.sh b/setup.sh index 4d14761..e47d197 100755 --- a/setup.sh +++ b/setup.sh @@ -254,7 +254,7 @@ check_prerequisites() { } select_branch() { - print_info "Fetching available branches from GitHub repository..." + print_info "Fetching available releases from GitHub repository..." # Create temporary directory for git operations TEMP_DIR="/tmp/patchmon_branches_$$" @@ -263,84 +263,88 @@ select_branch() { # Try to clone the repository normally if git clone "$DEFAULT_GITHUB_REPO" . 2>/dev/null; then - # Get list of remote branches and trim whitespace - branches=$(git branch -r | grep -v HEAD | sed 's/origin\///' | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//' | sort -u) + # Get list of tags sorted by version (semantic versioning) + # Using git tag with version sorting + tags=$(git tag -l --sort=-v:refname 2>/dev/null | head -3) - if [ -n "$branches" ]; then - print_info "Available branches with details:" + if [ -n "$tags" ]; then + print_info "Available releases and branches:" echo "" - # Get branch information - branch_count=1 - while IFS= read -r branch; do - if [ -n "$branch" ]; then - # Get last commit date for this branch - last_commit=$(git log -1 --format="%ci" "origin/$branch" 2>/dev/null || echo "Unknown") - - # Get release tag associated with this branch (if any) - release_tag=$(git describe --tags --exact-match "origin/$branch" 2>/dev/null || echo "") + # Display last 3 release tags + option_count=1 + declare -A options_map + + while IFS= read -r tag; do + if [ -n "$tag" ]; then + # Get tag date and commit info + tag_date=$(git log -1 --format="%ci" "$tag" 2>/dev/null || echo "Unknown") # Format the date - if [ "$last_commit" != "Unknown" ]; then - formatted_date=$(date -d "$last_commit" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$last_commit") + if [ "$tag_date" != "Unknown" ]; then + formatted_date=$(date -d "$tag_date" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$tag_date") else formatted_date="Unknown" fi - # Display branch info - printf "%2d. %-20s" "$branch_count" "$branch" - printf " (Last commit: %s)" "$formatted_date" - - if [ -n "$release_tag" ]; then - printf " [Release: %s]" "$release_tag" + # Mark the first one as latest + if [ $option_count -eq 1 ]; then + printf "%2d. %-20s (Latest Release - %s)\n" "$option_count" "$tag" "$formatted_date" + else + printf "%2d. %-20s (Release - %s)\n" "$option_count" "$tag" "$formatted_date" fi - echo "" - branch_count=$((branch_count + 1)) + # Store the tag for later selection + options_map[$option_count]="$tag" + option_count=$((option_count + 1)) fi - done <<< "$branches" + done <<< "$tags" + + # Add main branch as an option + main_commit=$(git log -1 --format="%ci" "origin/main" 2>/dev/null || echo "Unknown") + if [ "$main_commit" != "Unknown" ]; then + formatted_main_date=$(date -d "$main_commit" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$main_commit") + else + formatted_main_date="Unknown" + fi + printf "%2d. %-20s (Development Branch - %s)\n" "$option_count" "main" "$formatted_main_date" + options_map[$option_count]="main" echo "" - # Determine default selection: prefer 'main' if present - main_index=$(echo "$branches" | nl -w1 -s':' | awk -F':' '$2=="main"{print $1}' | head -1) - if [ -z "$main_index" ]; then - main_index=1 - fi + # Default to option 1 (latest release tag) + default_option=1 while true; do - read_input "Select branch number" BRANCH_NUMBER "$main_index" + read_input "Select version/branch number" SELECTION_NUMBER "$default_option" - if [[ "$BRANCH_NUMBER" =~ ^[0-9]+$ ]]; then - selected_branch=$(echo "$branches" | sed -n "${BRANCH_NUMBER}p" | sed 's/^[[:space:]]*//' | sed 's/[[:space:]]*$//') - if [ -n "$selected_branch" ]; then - DEPLOYMENT_BRANCH="$selected_branch" + if [[ "$SELECTION_NUMBER" =~ ^[0-9]+$ ]]; then + selected_option="${options_map[$SELECTION_NUMBER]}" + if [ -n "$selected_option" ]; then + DEPLOYMENT_BRANCH="$selected_option" - # Show additional info for selected branch - last_commit=$(git log -1 --format="%ci" "origin/$selected_branch" 2>/dev/null || echo "Unknown") - release_tag=$(git describe --tags --exact-match "origin/$selected_branch" 2>/dev/null || echo "") - - if [ "$last_commit" != "Unknown" ]; then - formatted_date=$(date -d "$last_commit" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$last_commit") + # Show confirmation + if [ "$selected_option" = "main" ]; then + print_status "Selected branch: main (latest development code)" + print_info "Last commit: $formatted_main_date" else - formatted_date="Unknown" - fi - - print_status "Selected branch: $DEPLOYMENT_BRANCH" - print_info "Last commit: $formatted_date" - if [ -n "$release_tag" ]; then - print_info "Release tag: $release_tag" + print_status "Selected release: $selected_option" + tag_date=$(git log -1 --format="%ci" "$selected_option" 2>/dev/null || echo "Unknown") + if [ "$tag_date" != "Unknown" ]; then + formatted_date=$(date -d "$tag_date" "+%Y-%m-%d %H:%M" 2>/dev/null || echo "$tag_date") + print_info "Release date: $formatted_date" + fi fi break else - print_error "Invalid branch number. Please try again." + print_error "Invalid selection number. Please try again." fi else print_error "Please enter a valid number." fi done else - print_warning "No branches found, using default: main" + print_warning "No release tags found, using default: main" DEPLOYMENT_BRANCH="main" fi else From 1fa0502d7dd9b38145fd4f576e8110933228058d Mon Sep 17 00:00:00 2001 From: Muhammad Ibrahim Date: Sun, 5 Oct 2025 19:27:55 +0100 Subject: [PATCH 2/2] Modified setup.sh to cater for new environment variables Added missing env variables in the env.example file --- backend/env.example | 2 ++ setup.sh | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/backend/env.example b/backend/env.example index 1db1fe7..6ef6243 100644 --- a/backend/env.example +++ b/backend/env.example @@ -1,5 +1,7 @@ # Database Configuration DATABASE_URL="postgresql://patchmon_user:p@tchm0n_p@55@localhost:5432/patchmon_db" +PM_DB_CONN_MAX_ATTEMPTS=30 +PM_DB_CONN_WAIT_INTERVAL=2 # Server Configuration PORT=3001 diff --git a/setup.sh b/setup.sh index e47d197..0908e14 100755 --- a/setup.sh +++ b/setup.sh @@ -793,9 +793,13 @@ create_env_files() { cat > backend/.env << EOF # Database Configuration DATABASE_URL="postgresql://$DB_USER:$DB_PASS@localhost:5432/$DB_NAME" +PM_DB_CONN_MAX_ATTEMPTS=30 +PM_DB_CONN_WAIT_INTERVAL=2 # JWT Configuration JWT_SECRET="$JWT_SECRET" +JWT_EXPIRES_IN=1h +JWT_REFRESH_EXPIRES_IN=7d # Server Configuration PORT=$BACKEND_PORT @@ -807,6 +811,12 @@ API_VERSION=v1 # CORS Configuration CORS_ORIGIN="$SERVER_PROTOCOL_SEL://$FQDN" +# Session Configuration +SESSION_INACTIVITY_TIMEOUT_MINUTES=30 + +# User Configuration +DEFAULT_USER_ROLE=user + # Rate Limiting (times in milliseconds) RATE_LIMIT_WINDOW_MS=900000 RATE_LIMIT_MAX=5000 @@ -817,6 +827,7 @@ AGENT_RATE_LIMIT_MAX=1000 # Logging LOG_LEVEL=info +ENABLE_LOGGING=true EOF # Frontend .env