Added ability to edit autoenrolment tokens once created

This commit is contained in:
Muhammad Ibrahim
2026-01-04 15:01:55 +00:00
parent c6d07121a6
commit 01693d7e34
2 changed files with 124 additions and 26 deletions

View File

@@ -344,9 +344,16 @@ router.patch(
authenticateToken,
requireManageSettings,
[
body("token_name")
.optional()
.isLength({ min: 1, max: 255 })
.withMessage("Token name must be between 1 and 255 characters"),
body("is_active").optional().isBoolean(),
body("max_hosts_per_day").optional().isInt({ min: 1, max: 1000 }),
body("allowed_ip_ranges").optional().isArray(),
body("default_host_group_id")
.optional({ nullable: true, checkFalsy: true })
.isString(),
body("expires_at").optional().isISO8601(),
body("scopes")
.optional()
@@ -373,6 +380,9 @@ router.patch(
const update_data = { updated_at: new Date() };
// Allow updating token name
if (req.body.token_name !== undefined)
update_data.token_name = req.body.token_name;
if (req.body.is_active !== undefined)
update_data.is_active = req.body.is_active;
if (req.body.max_hosts_per_day !== undefined)
@@ -382,6 +392,25 @@ router.patch(
if (req.body.expires_at !== undefined)
update_data.expires_at = new Date(req.body.expires_at);
// Handle default host group update
if (req.body.default_host_group_id !== undefined) {
if (req.body.default_host_group_id) {
// Validate host group exists
const host_group = await prisma.host_groups.findUnique({
where: { id: req.body.default_host_group_id },
});
if (!host_group) {
return res.status(400).json({ error: "Host group not found" });
}
update_data.default_host_group_id = req.body.default_host_group_id;
} else {
// Allow clearing the default host group
update_data.default_host_group_id = null;
}
}
// Handle scopes updates for API tokens only
if (req.body.scopes !== undefined) {
if (existing_token.metadata?.integration_type === "api") {
@@ -421,9 +450,16 @@ router.patch(
where: { id: tokenId },
data: update_data,
include: {
host_groups: true,
host_groups: {
select: {
id: true,
name: true,
color: true,
},
},
users: {
select: {
id: true,
username: true,
first_name: true,
last_name: true,

View File

@@ -13,7 +13,7 @@ import {
} from "lucide-react";
import { useEffect, useId, useState } from "react";
import SettingsLayout from "../../components/SettingsLayout";
import api from "../../utils/api";
import api, { formatDate } from "../../utils/api";
const Integrations = () => {
// Generate unique IDs for form elements
@@ -235,11 +235,20 @@ const Integrations = () => {
try {
const data = {
token_name: form_data.token_name,
max_hosts_per_day: form_data.max_hosts_per_day,
allowed_ip_ranges: form_data.allowed_ip_ranges
? form_data.allowed_ip_ranges.split(",").map((ip) => ip.trim())
: [],
};
// Add default host group if provided
if (form_data.default_host_group_id) {
data.default_host_group_id = form_data.default_host_group_id;
} else {
data.default_host_group_id = null;
}
// Add expiration if provided
if (form_data.expires_at) {
data.expires_at = form_data.expires_at;
@@ -323,7 +332,7 @@ const Integrations = () => {
}
};
const format_date = (date_string) => {
const formatDate = (date_string) => {
if (!date_string) return "Never";
return new Date(date_string).toLocaleString();
};
@@ -579,15 +588,15 @@ const Integrations = () => {
{token.allowed_ip_ranges.join(", ")}
</p>
)}
<p>Created: {format_date(token.created_at)}</p>
<p>Created: {formatDate(token.created_at)}</p>
{token.last_used_at && (
<p>
Last Used: {format_date(token.last_used_at)}
Last Used: {formatDate(token.last_used_at)}
</p>
)}
{token.expires_at && (
<p>
Expires: {format_date(token.expires_at)}
Expires: {formatDate(token.expires_at)}
{new Date(token.expires_at) <
new Date() && (
<span className="ml-2 text-red-600 dark:text-red-400">
@@ -599,15 +608,13 @@ const Integrations = () => {
</div>
</div>
<div className="flex items-center gap-2 flex-wrap w-full sm:w-auto">
{token.metadata?.integration_type === "api" && (
<button
type="button"
onClick={() => open_edit_modal(token)}
className="px-3 py-1 text-xs md:text-sm rounded bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300"
>
Edit
</button>
)}
<button
type="button"
onClick={() => open_edit_modal(token)}
className="px-3 py-1 text-xs md:text-sm rounded bg-blue-100 text-blue-700 hover:bg-blue-200 dark:bg-blue-900 dark:text-blue-300 dark:hover:bg-blue-800"
>
Edit
</button>
<button
type="button"
onClick={() =>
@@ -791,15 +798,15 @@ const Integrations = () => {
)}
</button>
</div>
<p>Created: {format_date(token.created_at)}</p>
<p>Created: {formatDate(token.created_at)}</p>
{token.last_used_at && (
<p>
Last Used: {format_date(token.last_used_at)}
Last Used: {formatDate(token.last_used_at)}
</p>
)}
{token.expires_at && (
<p>
Expires: {format_date(token.expires_at)}
Expires: {formatDate(token.expires_at)}
{new Date(token.expires_at) <
new Date() && (
<span className="ml-2 text-red-600 dark:text-red-400">
@@ -1821,14 +1828,14 @@ const Integrations = () => {
</div>
)}
{/* Edit API Credential Modal */}
{/* Edit Token Modal */}
{show_edit_modal && edit_token && (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4">
<div className="bg-white dark:bg-secondary-800 rounded-lg max-w-2xl w-full max-h-[90vh] overflow-y-auto">
<div className="p-4 md:p-6">
<div className="flex items-center justify-between mb-4 md:mb-6 gap-3">
<h2 className="text-lg md:text-xl font-bold text-secondary-900 dark:text-white">
Edit API Credential
Edit Token
</h2>
<button
type="button"
@@ -1843,21 +1850,76 @@ const Integrations = () => {
</div>
<form onSubmit={update_token} className="space-y-4">
<div className="block">
<label className="block">
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Token Name
</span>
<input
type="text"
value={form_data.token_name}
readOnly
disabled
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-secondary-100 dark:bg-secondary-900 text-secondary-500 dark:text-secondary-400"
onChange={(e) =>
setFormData({ ...form_data, token_name: e.target.value })
}
placeholder="e.g., my-pve"
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
required
/>
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
Token name cannot be changed
Update the token name for better organization
</p>
</div>
</label>
<label className="block">
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Max Hosts Per Day
</span>
<input
type="number"
min="1"
max="1000"
value={form_data.max_hosts_per_day}
onChange={(e) =>
setFormData({
...form_data,
max_hosts_per_day: parseInt(e.target.value, 10) || 100,
})
}
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
/>
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
Maximum number of hosts that can be enrolled per day with
this token
</p>
</label>
{(edit_token?.metadata?.integration_type === "proxmox-lxc" ||
edit_token?.metadata?.integration_type === "direct-host") && (
<label className="block">
<span className="block text-sm font-medium text-secondary-700 dark:text-secondary-300 mb-1">
Default Host Group (Optional)
</span>
<select
value={form_data.default_host_group_id}
onChange={(e) =>
setFormData({
...form_data,
default_host_group_id: e.target.value,
})
}
className="w-full px-3 py-2 border border-secondary-300 dark:border-secondary-600 rounded-md bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
>
<option value="">No default group</option>
{host_groups.map((group) => (
<option key={group.id} value={group.id}>
{group.name}
</option>
))}
</select>
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
Auto-enrolled hosts will be assigned to this group
</p>
</label>
)}
{edit_token?.metadata?.integration_type === "api" && (
<div className="block">