mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-10-23 07:42:05 +00:00
Merge pull request #137 from PatchMon/dev
Fixed Profile Name editing issue where it wouldn't save Added more environment variables to env.example fixed setup.sh so it would ask for the release tag rather than just the branch
This commit is contained in:
@@ -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
|
||||
|
@@ -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,
|
||||
|
@@ -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 }) => {
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{success && (
|
||||
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-md p-3">
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400 mr-2" />
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
User created successfully!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-danger-50 dark:bg-danger-900 border border-danger-200 dark:border-danger-700 rounded-md p-3">
|
||||
<p className="text-sm text-danger-700 dark:text-danger-300">
|
||||
@@ -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 }) => {
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{success && (
|
||||
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-md p-3">
|
||||
<div className="flex items-center">
|
||||
<CheckCircle className="h-4 w-4 text-green-600 dark:text-green-400 mr-2" />
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
User updated successfully!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{error && (
|
||||
<div className="bg-danger-50 dark:bg-danger-900 border border-danger-200 dark:border-danger-700 rounded-md p-3">
|
||||
<p className="text-sm text-danger-700 dark:text-danger-300">
|
||||
|
@@ -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: "",
|
||||
|
115
setup.sh
115
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
|
||||
@@ -789,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
|
||||
@@ -803,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
|
||||
@@ -813,6 +827,7 @@ AGENT_RATE_LIMIT_MAX=1000
|
||||
|
||||
# Logging
|
||||
LOG_LEVEL=info
|
||||
ENABLE_LOGGING=true
|
||||
EOF
|
||||
|
||||
# Frontend .env
|
||||
|
Reference in New Issue
Block a user