mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-12-31 01:59:33 -06:00
@@ -0,0 +1,22 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "release_notes_acceptances" (
|
||||
"id" TEXT NOT NULL,
|
||||
"user_id" TEXT NOT NULL,
|
||||
"version" TEXT NOT NULL,
|
||||
"accepted_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
CONSTRAINT "release_notes_acceptances_pkey" PRIMARY KEY ("id")
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "release_notes_acceptances_user_id_version_key" ON "release_notes_acceptances"("user_id", "version");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "release_notes_acceptances_user_id_idx" ON "release_notes_acceptances"("user_id");
|
||||
|
||||
-- CreateIndex
|
||||
CREATE INDEX "release_notes_acceptances_version_idx" ON "release_notes_acceptances"("version");
|
||||
|
||||
-- AddForeignKey
|
||||
ALTER TABLE "release_notes_acceptances" ADD CONSTRAINT "release_notes_acceptances_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "settings" ADD COLUMN "show_github_version_on_login" BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
@@ -202,6 +202,7 @@ model settings {
|
||||
metrics_enabled Boolean @default(true)
|
||||
metrics_anonymous_id String?
|
||||
metrics_last_sent DateTime?
|
||||
show_github_version_on_login Boolean @default(true)
|
||||
}
|
||||
|
||||
model update_history {
|
||||
@@ -250,6 +251,7 @@ model users {
|
||||
dashboard_preferences dashboard_preferences[]
|
||||
user_sessions user_sessions[]
|
||||
auto_enrollment_tokens auto_enrollment_tokens[]
|
||||
release_notes_acceptances release_notes_acceptances[]
|
||||
}
|
||||
|
||||
model user_sessions {
|
||||
@@ -437,3 +439,15 @@ model job_history {
|
||||
@@index([status])
|
||||
@@index([created_at])
|
||||
}
|
||||
|
||||
model release_notes_acceptances {
|
||||
id String @id @default(uuid())
|
||||
user_id String
|
||||
version String
|
||||
accepted_at DateTime @default(now())
|
||||
users users @relation(fields: [user_id], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([user_id, version])
|
||||
@@index([user_id])
|
||||
@@index([version])
|
||||
}
|
||||
|
||||
34
backend/release-notes/RELEASE_NOTES_1.3.7.md
Normal file
34
backend/release-notes/RELEASE_NOTES_1.3.7.md
Normal file
@@ -0,0 +1,34 @@
|
||||
## 📝 ALERT : Auto-update of Agent issue
|
||||
|
||||
Versions <1.3.6 have an issue where the service does not restart after auto-update. OpenRC systems are unaffected and work correctly.
|
||||
This means you will unfortunately have to use `systemctl start patchmon-agent` on your systems to load up 1.3.7 agent when it auto-updates shortly.
|
||||
|
||||
Very sorry for this, future versions are fixed - I built this release notes notification feature specifically to notify you of this.
|
||||
|
||||
---
|
||||
|
||||
## 🎉 New Features & Improvements :
|
||||
|
||||
**Mobile UI**: Mobile user interface improvements are mostly complete, providing a better experience on mobile devices.
|
||||
|
||||
**Systemctl Helper Script**: In future versions (1.3.7+), a systemctl helper script will be available to assist with auto-update service restarts.
|
||||
|
||||
**Staggered Agent Intervals**: Agents now report at staggered times to prevent overwhelming the PatchMon server. If the agent report interval is set to 60 minutes, different hosts will report at different times. This is in the `config.yml` as "report_offset: nxyz"
|
||||
|
||||
**Reboot Detection Information**: Reboot detection information is now stored in the database. When the "Reboot Required" flag is displayed, hovering over it will show the specific reason why a reboot is needed (Reboot feature still needs work and it will be much better in 1.3.8)
|
||||
|
||||
**JSON Report Output**: The `patchmon-agent report --json` command now outputs the complete report payload to the console in JSON format instead of sending it to the PatchMon server. This is very useful for integrating PatchMon agent data with other tools and for diagnostic purposes.
|
||||
|
||||
**Persistent Docker Toggle**: Docker integration toggle state is now persisted in the database, eliminating in-memory configuration issues. No more losing Docker settings on container restarts (thanks to the community for initiating this feature).
|
||||
|
||||
**Config.yml Synchronization**: The agent now writes and compares the `config.yml` file with the server configuration upon startup, ensuring better synchronization of settings between the agent and server.
|
||||
|
||||
**Network Information Page**: Enhanced network information page to display IPv6 addresses and support multiple network interfaces, providing more comprehensive network details.
|
||||
|
||||
**Auto-Update Logic Fix**: Fixed an issue where agents would auto-update even when per-host auto-update was disabled. The logic now properly honors both server-wide auto-update settings and per-host auto-update settings.
|
||||
|
||||
**Prisma Version Fix**: Fixed Prisma version issues affecting Kubernetes deployments by statically setting the Prisma version in package.json files.
|
||||
|
||||
**Hiding Github Version**: Added a toggle in Server Version settings to disable showing the github release notes on the login screen
|
||||
|
||||
---
|
||||
@@ -844,6 +844,24 @@ router.post(
|
||||
req,
|
||||
);
|
||||
|
||||
// Get accepted release notes versions
|
||||
let acceptedVersions = [];
|
||||
try {
|
||||
if (prisma.release_notes_acceptances) {
|
||||
acceptedVersions = await prisma.release_notes_acceptances.findMany({
|
||||
where: { user_id: user.id },
|
||||
select: { version: true },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// If table doesn't exist yet or Prisma client not regenerated, use empty array
|
||||
console.warn(
|
||||
"Could not fetch release notes acceptances:",
|
||||
error.message,
|
||||
);
|
||||
acceptedVersions = [];
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: "Login successful",
|
||||
token: session.access_token,
|
||||
@@ -863,6 +881,9 @@ router.post(
|
||||
// Include user preferences so they're available immediately after login
|
||||
theme_preference: user.theme_preference,
|
||||
color_theme: user.color_theme,
|
||||
accepted_release_notes_versions: acceptedVersions.map(
|
||||
(a) => a.version,
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -986,13 +1007,36 @@ router.post(
|
||||
req,
|
||||
);
|
||||
|
||||
// Get accepted release notes versions
|
||||
let acceptedVersions = [];
|
||||
try {
|
||||
if (prisma.release_notes_acceptances) {
|
||||
acceptedVersions = await prisma.release_notes_acceptances.findMany({
|
||||
where: { user_id: user.id },
|
||||
select: { version: true },
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
// If table doesn't exist yet or Prisma client not regenerated, use empty array
|
||||
console.warn(
|
||||
"Could not fetch release notes acceptances:",
|
||||
error.message,
|
||||
);
|
||||
acceptedVersions = [];
|
||||
}
|
||||
|
||||
res.json({
|
||||
message: "Login successful",
|
||||
token: session.access_token,
|
||||
refresh_token: session.refresh_token,
|
||||
expires_at: session.expires_at,
|
||||
tfa_bypass_until: session.tfa_bypass_until,
|
||||
user: updatedUser,
|
||||
user: {
|
||||
...updatedUser,
|
||||
accepted_release_notes_versions: acceptedVersions.map(
|
||||
(a) => a.version,
|
||||
),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("TFA verification error:", error);
|
||||
|
||||
206
backend/src/routes/buyMeACoffeeRoutes.js
Normal file
206
backend/src/routes/buyMeACoffeeRoutes.js
Normal file
@@ -0,0 +1,206 @@
|
||||
const express = require("express");
|
||||
const axios = require("axios");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Cache for supporter count (5 minute cache)
|
||||
const supporterCountCache = {
|
||||
count: null,
|
||||
timestamp: null,
|
||||
cacheDuration: 5 * 60 * 1000, // 5 minutes
|
||||
};
|
||||
|
||||
// Get supporter count from Buy Me a Coffee page
|
||||
router.get("/supporter-count", authenticateToken, async (_req, res) => {
|
||||
try {
|
||||
// Check cache first
|
||||
const now = Date.now();
|
||||
if (
|
||||
supporterCountCache.count !== null &&
|
||||
supporterCountCache.timestamp !== null &&
|
||||
now - supporterCountCache.timestamp < supporterCountCache.cacheDuration
|
||||
) {
|
||||
return res.json({ count: supporterCountCache.count, cached: true });
|
||||
}
|
||||
|
||||
// Fetch the Buy Me a Coffee page
|
||||
const response = await axios.get("https://buymeacoffee.com/iby___", {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
||||
},
|
||||
timeout: 10000, // 10 second timeout
|
||||
});
|
||||
|
||||
const html = response.data;
|
||||
|
||||
// Try multiple patterns to find the supporter count
|
||||
let supporterCount = null;
|
||||
|
||||
// Pattern 1: Look for "X supporters" or "X supporter" text (most common)
|
||||
// Try various formats: "25 supporters", "1 supporter", "25 people", etc.
|
||||
const textPatterns = [
|
||||
/(\d+)\s+supporters?/i,
|
||||
/(\d+)\s+people\s+(have\s+)?(bought|supported)/i,
|
||||
/supporter[^>]*>.*?(\d+)/i,
|
||||
/(\d+)[^<]*supporter/i,
|
||||
/>(\d+)<[^>]*supporter/i,
|
||||
/supporter[^<]*<[^>]*>(\d+)/i,
|
||||
];
|
||||
|
||||
for (const pattern of textPatterns) {
|
||||
const match = html.match(pattern);
|
||||
if (match?.[1]) {
|
||||
const count = parseInt(match[1], 10);
|
||||
if (count > 0 && count < 1000000) {
|
||||
// Reasonable upper limit
|
||||
supporterCount = count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 2: Look for data attributes or specific class names
|
||||
// Buy Me a Coffee might use data attributes like data-count, data-supporters, etc.
|
||||
if (!supporterCount) {
|
||||
const dataPatterns = [
|
||||
/data-supporters?=["'](\d+)["']/i,
|
||||
/data-count=["'](\d+)["']/i,
|
||||
/supporter[^>]*data-[^=]*=["'](\d+)["']/i,
|
||||
];
|
||||
|
||||
for (const pattern of dataPatterns) {
|
||||
const match = html.match(pattern);
|
||||
if (match?.[1]) {
|
||||
const count = parseInt(match[1], 10);
|
||||
if (count > 0 && count < 1000000) {
|
||||
supporterCount = count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 3: Look for JSON-LD structured data or meta tags
|
||||
if (!supporterCount) {
|
||||
const jsonLdMatches = html.matchAll(
|
||||
/<script[^>]*type=["']application\/ld\+json["'][^>]*>(.*?)<\/script>/gis,
|
||||
);
|
||||
for (const jsonLdMatch of jsonLdMatches) {
|
||||
try {
|
||||
const jsonLd = JSON.parse(jsonLdMatch[1]);
|
||||
// Look for supporter count in structured data (could be nested)
|
||||
const findCount = (obj) => {
|
||||
if (typeof obj !== "object" || obj === null) return null;
|
||||
if (obj.supporterCount || obj.supporter_count || obj.supporters) {
|
||||
return parseInt(
|
||||
obj.supporterCount || obj.supporter_count || obj.supporters,
|
||||
10,
|
||||
);
|
||||
}
|
||||
for (const value of Object.values(obj)) {
|
||||
if (typeof value === "object") {
|
||||
const found = findCount(value);
|
||||
if (found) return found;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
const count = findCount(jsonLd);
|
||||
if (count && count > 0 && count < 1000000) {
|
||||
supporterCount = count;
|
||||
break;
|
||||
}
|
||||
} catch (_e) {
|
||||
// Ignore JSON parse errors
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 4: Look for specific class names or IDs that Buy Me a Coffee uses
|
||||
if (!supporterCount) {
|
||||
const classPatterns = [
|
||||
/class="[^"]*supporter[^"]*"[^>]*>.*?(\d+)/i,
|
||||
/id="[^"]*supporter[^"]*"[^>]*>.*?(\d+)/i,
|
||||
/<span[^>]*class="[^"]*count[^"]*"[^>]*>(\d+)<\/span>/i,
|
||||
];
|
||||
|
||||
for (const pattern of classPatterns) {
|
||||
const match = html.match(pattern);
|
||||
if (match?.[1]) {
|
||||
const count = parseInt(match[1], 10);
|
||||
if (count > 0 && count < 1000000) {
|
||||
supporterCount = count;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pattern 5: Look for numbers near common supporter-related text
|
||||
if (!supporterCount) {
|
||||
// Find all numbers in the HTML and check context around them
|
||||
const numberMatches = html.matchAll(/\b(\d{1,6})\b/g);
|
||||
for (const match of numberMatches) {
|
||||
const num = parseInt(match[1], 10);
|
||||
if (num > 0 && num < 1000000) {
|
||||
// Check context around this number (200 chars before and after)
|
||||
const start = Math.max(0, match.index - 200);
|
||||
const end = Math.min(
|
||||
html.length,
|
||||
match.index + match[0].length + 200,
|
||||
);
|
||||
const context = html.substring(start, end).toLowerCase();
|
||||
if (
|
||||
context.includes("supporter") ||
|
||||
context.includes("coffee") ||
|
||||
context.includes("donation")
|
||||
) {
|
||||
supporterCount = num;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update cache
|
||||
if (supporterCount !== null && supporterCount > 0) {
|
||||
supporterCountCache.count = supporterCount;
|
||||
supporterCountCache.timestamp = now;
|
||||
}
|
||||
|
||||
if (supporterCount === null) {
|
||||
// If we couldn't parse it, return the cached value if available, or 0
|
||||
if (supporterCountCache.count !== null) {
|
||||
return res.json({
|
||||
count: supporterCountCache.count,
|
||||
cached: true,
|
||||
error: "Could not parse current count, returning cached value",
|
||||
});
|
||||
}
|
||||
return res.status(500).json({
|
||||
error: "Could not retrieve supporter count",
|
||||
});
|
||||
}
|
||||
|
||||
res.json({ count: supporterCount, cached: false });
|
||||
} catch (error) {
|
||||
console.error("Error fetching Buy Me a Coffee supporter count:", error);
|
||||
|
||||
// Return cached value if available
|
||||
if (supporterCountCache.count !== null) {
|
||||
return res.json({
|
||||
count: supporterCountCache.count,
|
||||
cached: true,
|
||||
error: "Failed to fetch, returning cached value",
|
||||
});
|
||||
}
|
||||
|
||||
res.status(500).json({
|
||||
error: "Failed to fetch supporter count",
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
53
backend/src/routes/releaseNotesAcceptanceRoutes.js
Normal file
53
backend/src/routes/releaseNotesAcceptanceRoutes.js
Normal file
@@ -0,0 +1,53 @@
|
||||
const express = require("express");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
const { getPrismaClient } = require("../config/prisma");
|
||||
|
||||
const prisma = getPrismaClient();
|
||||
const router = express.Router();
|
||||
|
||||
// Mark release notes as accepted for current user
|
||||
router.post("/accept", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const userId = req.user.id;
|
||||
const { version } = req.body;
|
||||
|
||||
if (!version) {
|
||||
return res.status(400).json({ error: "Version is required" });
|
||||
}
|
||||
|
||||
// Check if the model exists (Prisma client might not be regenerated yet)
|
||||
if (!prisma.release_notes_acceptances) {
|
||||
console.warn(
|
||||
"release_notes_acceptances model not available - Prisma client may need regeneration",
|
||||
);
|
||||
return res.status(503).json({
|
||||
error:
|
||||
"Release notes acceptance feature not available. Please regenerate Prisma client.",
|
||||
});
|
||||
}
|
||||
|
||||
await prisma.release_notes_acceptances.upsert({
|
||||
where: {
|
||||
user_id_version: {
|
||||
user_id: userId,
|
||||
version: version,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
accepted_at: new Date(),
|
||||
},
|
||||
create: {
|
||||
user_id: userId,
|
||||
version: version,
|
||||
accepted_at: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("Error accepting release notes:", error);
|
||||
res.status(500).json({ error: "Failed to accept release notes" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
104
backend/src/routes/releaseNotesRoutes.js
Normal file
104
backend/src/routes/releaseNotesRoutes.js
Normal file
@@ -0,0 +1,104 @@
|
||||
const express = require("express");
|
||||
const fs = require("node:fs").promises;
|
||||
const path = require("node:path");
|
||||
const { authenticateToken } = require("../middleware/auth");
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Helper to get current version
|
||||
function getCurrentVersion() {
|
||||
try {
|
||||
const packageJson = require("../../package.json");
|
||||
return packageJson?.version || "unknown";
|
||||
} catch (error) {
|
||||
console.error("Could not read version from package.json:", error);
|
||||
return "unknown";
|
||||
}
|
||||
}
|
||||
|
||||
// Get release notes for a specific version
|
||||
router.get("/:version", authenticateToken, async (req, res) => {
|
||||
try {
|
||||
const { version } = req.params;
|
||||
const releaseNotesPath = path.join(
|
||||
__dirname,
|
||||
"../../release-notes",
|
||||
`RELEASE_NOTES_${version}.md`,
|
||||
);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(releaseNotesPath, "utf-8");
|
||||
res.json({
|
||||
version,
|
||||
content,
|
||||
exists: true,
|
||||
});
|
||||
} catch (_fileError) {
|
||||
// File doesn't exist for this version
|
||||
res.json({
|
||||
version,
|
||||
content: null,
|
||||
exists: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching release notes:", error);
|
||||
res.status(500).json({ error: "Failed to fetch release notes" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get release notes for current version
|
||||
router.get("/current", authenticateToken, async (_req, res) => {
|
||||
try {
|
||||
const currentVersion = getCurrentVersion();
|
||||
const releaseNotesPath = path.join(
|
||||
__dirname,
|
||||
"../../release-notes",
|
||||
`RELEASE_NOTES_${currentVersion}.md`,
|
||||
);
|
||||
|
||||
try {
|
||||
const content = await fs.readFile(releaseNotesPath, "utf-8");
|
||||
res.json({
|
||||
version: currentVersion,
|
||||
content,
|
||||
exists: true,
|
||||
});
|
||||
} catch (_fileError) {
|
||||
// No release notes for current version
|
||||
res.json({
|
||||
version: currentVersion,
|
||||
content: null,
|
||||
exists: false,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching current release notes:", error);
|
||||
res.status(500).json({ error: "Failed to fetch release notes" });
|
||||
}
|
||||
});
|
||||
|
||||
// List all available release notes versions
|
||||
router.get("/", authenticateToken, async (_req, res) => {
|
||||
try {
|
||||
const releaseNotesDir = path.join(__dirname, "../../release-notes");
|
||||
const files = await fs.readdir(releaseNotesDir);
|
||||
|
||||
const versions = files
|
||||
.filter(
|
||||
(file) => file.startsWith("RELEASE_NOTES_") && file.endsWith(".md"),
|
||||
)
|
||||
.map((file) => file.replace("RELEASE_NOTES_", "").replace(".md", ""))
|
||||
.sort((a, b) => {
|
||||
// Simple version comparison (you might want a more robust one)
|
||||
return b.localeCompare(a, undefined, { numeric: true });
|
||||
});
|
||||
|
||||
res.json({ versions });
|
||||
} catch (error) {
|
||||
console.error("Error listing release notes:", error);
|
||||
res.status(500).json({ error: "Failed to list release notes" });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -134,6 +134,10 @@ router.put(
|
||||
.optional()
|
||||
.isLength({ min: 1 })
|
||||
.withMessage("Favicon path must be a non-empty string"),
|
||||
body("showGithubVersionOnLogin")
|
||||
.optional()
|
||||
.isBoolean()
|
||||
.withMessage("Show GitHub version on login must be a boolean"),
|
||||
],
|
||||
async (req, res) => {
|
||||
try {
|
||||
@@ -159,6 +163,7 @@ router.put(
|
||||
logoLight,
|
||||
favicon,
|
||||
colorTheme,
|
||||
showGithubVersionOnLogin,
|
||||
} = req.body;
|
||||
|
||||
// Get current settings to check for update interval changes
|
||||
@@ -191,6 +196,8 @@ router.put(
|
||||
if (logoLight !== undefined) updateData.logo_light = logoLight;
|
||||
if (favicon !== undefined) updateData.favicon = favicon;
|
||||
if (colorTheme !== undefined) updateData.color_theme = colorTheme;
|
||||
if (showGithubVersionOnLogin !== undefined)
|
||||
updateData.show_github_version_on_login = showGithubVersionOnLogin;
|
||||
|
||||
const updatedSettings = await updateSettings(
|
||||
currentSettings.id,
|
||||
@@ -254,6 +261,21 @@ router.get("/server-url", async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Get login settings for public use (used by login screen)
|
||||
router.get("/login-settings", async (_req, res) => {
|
||||
try {
|
||||
const settings = await getSettings();
|
||||
res.json({
|
||||
show_github_version_on_login:
|
||||
settings.show_github_version_on_login !== false,
|
||||
signup_enabled: settings.signup_enabled || false,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch login settings:", error);
|
||||
res.status(500).json({ error: "Failed to fetch login settings" });
|
||||
}
|
||||
});
|
||||
|
||||
// Get update interval policy for agents (requires API authentication)
|
||||
router.get("/update-interval", async (req, res) => {
|
||||
try {
|
||||
|
||||
@@ -72,6 +72,9 @@ const agentVersionRoutes = require("./routes/agentVersionRoutes");
|
||||
const metricsRoutes = require("./routes/metricsRoutes");
|
||||
const userPreferencesRoutes = require("./routes/userPreferencesRoutes");
|
||||
const apiHostsRoutes = require("./routes/apiHostsRoutes");
|
||||
const releaseNotesRoutes = require("./routes/releaseNotesRoutes");
|
||||
const releaseNotesAcceptanceRoutes = require("./routes/releaseNotesAcceptanceRoutes");
|
||||
const buyMeACoffeeRoutes = require("./routes/buyMeACoffeeRoutes");
|
||||
const { initSettings } = require("./services/settingsService");
|
||||
const { queueManager } = require("./services/automation");
|
||||
const { authenticateToken, requireAdmin } = require("./middleware/auth");
|
||||
@@ -482,6 +485,12 @@ app.use(`/api/${apiVersion}/agent`, agentVersionRoutes);
|
||||
app.use(`/api/${apiVersion}/metrics`, metricsRoutes);
|
||||
app.use(`/api/${apiVersion}/user/preferences`, userPreferencesRoutes);
|
||||
app.use(`/api/${apiVersion}/api`, authLimiter, apiHostsRoutes);
|
||||
app.use(`/api/${apiVersion}/release-notes`, releaseNotesRoutes);
|
||||
app.use(
|
||||
`/api/${apiVersion}/release-notes-acceptance`,
|
||||
releaseNotesAcceptanceRoutes,
|
||||
);
|
||||
app.use(`/api/${apiVersion}/buy-me-a-coffee`, buyMeACoffeeRoutes);
|
||||
|
||||
// Bull Board - will be populated after queue manager initializes
|
||||
let bullBoardRouter = null;
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
// Common utilities for automation jobs
|
||||
const path = require("node:path");
|
||||
const fs = require("node:fs");
|
||||
|
||||
/**
|
||||
* Compare two semantic versions
|
||||
@@ -36,7 +38,10 @@ async function checkPublicRepo(owner, repo) {
|
||||
// Get current version for User-Agent (or use generic if unavailable)
|
||||
let currentVersion = "unknown";
|
||||
try {
|
||||
const packageJson = require("../../../package.json");
|
||||
// Use __dirname to construct absolute path to package.json
|
||||
const packageJsonPath = path.join(__dirname, "../../../package.json");
|
||||
const packageJsonContent = fs.readFileSync(packageJsonPath, "utf8");
|
||||
const packageJson = JSON.parse(packageJsonContent);
|
||||
if (packageJson?.version) {
|
||||
currentVersion = packageJson.version;
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"react-chartjs-2": "^5.2.0",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-icons": "^5.5.0",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-router-dom": "^6.30.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -35,6 +35,7 @@ import { dashboardAPI, versionAPI } from "../utils/api";
|
||||
import DiscordIcon from "./DiscordIcon";
|
||||
import GlobalSearch from "./GlobalSearch";
|
||||
import Logo from "./Logo";
|
||||
import ReleaseNotesModal from "./ReleaseNotesModal";
|
||||
import UpgradeNotificationIcon from "./UpgradeNotificationIcon";
|
||||
|
||||
const Layout = ({ children }) => {
|
||||
@@ -47,6 +48,7 @@ const Layout = ({ children }) => {
|
||||
const [_userMenuOpen, setUserMenuOpen] = useState(false);
|
||||
const [githubStars, setGithubStars] = useState(null);
|
||||
const [mobileLinksOpen, setMobileLinksOpen] = useState(false);
|
||||
const [showReleaseNotes, setShowReleaseNotes] = useState(false);
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const {
|
||||
@@ -86,6 +88,37 @@ const Layout = ({ children }) => {
|
||||
staleTime: 300000, // Consider data stale after 5 minutes
|
||||
});
|
||||
|
||||
// Check for new release notes when user or version changes
|
||||
useEffect(() => {
|
||||
if (!user || !versionInfo?.version) return;
|
||||
|
||||
// Get accepted versions from user object (loaded on login)
|
||||
const acceptedVersions = user.accepted_release_notes_versions || [];
|
||||
const currentVersion = versionInfo.version;
|
||||
|
||||
// If already accepted, don't show
|
||||
if (acceptedVersions.includes(currentVersion)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if release notes exist for this version
|
||||
fetch(`/api/v1/release-notes/${currentVersion}`, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${localStorage.getItem("token")}`,
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then((data) => {
|
||||
// Only show if release notes exist and version not accepted
|
||||
if (data.exists && !acceptedVersions.includes(currentVersion)) {
|
||||
setShowReleaseNotes(true);
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error("Error checking release notes:", error);
|
||||
});
|
||||
}, [user, versionInfo]);
|
||||
|
||||
// Build navigation based on permissions
|
||||
const buildNavigation = () => {
|
||||
const nav = [];
|
||||
@@ -1536,6 +1569,12 @@ const Layout = ({ children }) => {
|
||||
<div className="px-4 sm:px-6 lg:px-8">{children}</div>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
{/* Release Notes Modal */}
|
||||
<ReleaseNotesModal
|
||||
isOpen={showReleaseNotes}
|
||||
onAccept={() => setShowReleaseNotes(false)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
383
frontend/src/components/ReleaseNotesModal.jsx
Normal file
383
frontend/src/components/ReleaseNotesModal.jsx
Normal file
File diff suppressed because one or more lines are too long
@@ -1,19 +1,15 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import { AlertCircle, CheckCircle, Save, Shield, X } from "lucide-react";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { permissionsAPI, settingsAPI } from "../../utils/api";
|
||||
import { settingsAPI } from "../../utils/api";
|
||||
|
||||
const AgentUpdatesTab = () => {
|
||||
const updateIntervalId = useId();
|
||||
const autoUpdateId = useId();
|
||||
const signupEnabledId = useId();
|
||||
const defaultRoleId = useId();
|
||||
const ignoreSslId = useId();
|
||||
const [formData, setFormData] = useState({
|
||||
updateInterval: 60,
|
||||
autoUpdate: false,
|
||||
signupEnabled: false,
|
||||
defaultUserRole: "user",
|
||||
ignoreSslSelfSigned: false,
|
||||
});
|
||||
const [errors, setErrors] = useState({});
|
||||
@@ -82,20 +78,12 @@ const AgentUpdatesTab = () => {
|
||||
queryFn: () => settingsAPI.get().then((res) => res.data),
|
||||
});
|
||||
|
||||
// Fetch available roles for default user role dropdown
|
||||
const { data: roles, isLoading: rolesLoading } = useQuery({
|
||||
queryKey: ["rolePermissions"],
|
||||
queryFn: () => permissionsAPI.getRoles().then((res) => res.data),
|
||||
});
|
||||
|
||||
// Update form data when settings are loaded
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
const newFormData = {
|
||||
updateInterval: settings.update_interval || 60,
|
||||
autoUpdate: settings.auto_update || false,
|
||||
signupEnabled: settings.signup_enabled === true,
|
||||
defaultUserRole: settings.default_user_role || "user",
|
||||
ignoreSslSelfSigned: settings.ignore_ssl_self_signed === true,
|
||||
};
|
||||
setFormData(newFormData);
|
||||
@@ -425,85 +413,6 @@ const AgentUpdatesTab = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* User Signup Setting */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id={signupEnabledId}
|
||||
type="checkbox"
|
||||
checked={formData.signupEnabled}
|
||||
onChange={(e) =>
|
||||
handleInputChange("signupEnabled", e.target.checked)
|
||||
}
|
||||
className="rounded border-secondary-300 text-primary-600 shadow-sm focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50"
|
||||
/>
|
||||
<label htmlFor={signupEnabledId}>
|
||||
Enable User Self-Registration
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Default User Role Dropdown */}
|
||||
{formData.signupEnabled && (
|
||||
<div className="mt-3 ml-6">
|
||||
<label
|
||||
htmlFor={defaultRoleId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
|
||||
>
|
||||
Default Role for New Users
|
||||
</label>
|
||||
<select
|
||||
id={defaultRoleId}
|
||||
value={formData.defaultUserRole}
|
||||
onChange={(e) =>
|
||||
handleInputChange("defaultUserRole", e.target.value)
|
||||
}
|
||||
className="w-full max-w-xs border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
||||
disabled={rolesLoading}
|
||||
>
|
||||
{rolesLoading ? (
|
||||
<option>Loading roles...</option>
|
||||
) : roles && Array.isArray(roles) ? (
|
||||
roles.map((role) => (
|
||||
<option key={role.role} value={role.role}>
|
||||
{role.role.charAt(0).toUpperCase() + role.role.slice(1)}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="user">User</option>
|
||||
)}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||
New users will be assigned this role when they register.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
||||
When enabled, users can create their own accounts through the signup
|
||||
page. When disabled, only administrators can create user accounts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Security Notice */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<Shield className="h-5 w-5 text-blue-400 dark:text-blue-300" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Security Notice
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-blue-700 dark:text-blue-300">
|
||||
When enabling user self-registration, exercise caution on
|
||||
internal networks. Consider restricting access to trusted
|
||||
networks only and ensure proper role assignments to prevent
|
||||
unauthorized access to sensitive systems.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Edit,
|
||||
Key,
|
||||
Mail,
|
||||
Save,
|
||||
Shield,
|
||||
Trash2,
|
||||
User,
|
||||
@@ -12,7 +13,7 @@ import {
|
||||
} from "lucide-react";
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import { useAuth } from "../../contexts/AuthContext";
|
||||
import { adminUsersAPI, permissionsAPI } from "../../utils/api";
|
||||
import { adminUsersAPI, permissionsAPI, settingsAPI } from "../../utils/api";
|
||||
|
||||
const UsersTab = () => {
|
||||
const [showAddModal, setShowAddModal] = useState(false);
|
||||
@@ -20,6 +21,13 @@ const UsersTab = () => {
|
||||
const [resetPasswordUser, setResetPasswordUser] = useState(null);
|
||||
const queryClient = useQueryClient();
|
||||
const { user: currentUser } = useAuth();
|
||||
const signupEnabledId = useId();
|
||||
const defaultRoleId = useId();
|
||||
const [signupFormData, setSignupFormData] = useState({
|
||||
signupEnabled: false,
|
||||
defaultUserRole: "user",
|
||||
});
|
||||
const [isSignupDirty, setIsSignupDirty] = useState(false);
|
||||
|
||||
// Listen for the header button event to open add modal
|
||||
useEffect(() => {
|
||||
@@ -40,11 +48,28 @@ const UsersTab = () => {
|
||||
});
|
||||
|
||||
// Fetch available roles
|
||||
const { data: roles } = useQuery({
|
||||
const { data: roles, isLoading: rolesLoading } = useQuery({
|
||||
queryKey: ["rolePermissions"],
|
||||
queryFn: () => permissionsAPI.getRoles().then((res) => res.data),
|
||||
});
|
||||
|
||||
// Fetch current settings for user registration
|
||||
const { data: settings, isLoading: settingsLoading } = useQuery({
|
||||
queryKey: ["settings"],
|
||||
queryFn: () => settingsAPI.get().then((res) => res.data),
|
||||
});
|
||||
|
||||
// Update signup form data when settings are loaded
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
setSignupFormData({
|
||||
signupEnabled: settings.signup_enabled === true,
|
||||
defaultUserRole: settings.default_user_role || "user",
|
||||
});
|
||||
setIsSignupDirty(false);
|
||||
}
|
||||
}, [settings]);
|
||||
|
||||
// Delete user mutation
|
||||
const deleteUserMutation = useMutation({
|
||||
mutationFn: adminUsersAPI.delete,
|
||||
@@ -72,6 +97,32 @@ const UsersTab = () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Update settings mutation for user registration
|
||||
const updateSignupSettingsMutation = useMutation({
|
||||
mutationFn: (data) => {
|
||||
return settingsAPI.update(data).then((res) => res.data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["settings"]);
|
||||
setIsSignupDirty(false);
|
||||
},
|
||||
});
|
||||
|
||||
const handleSignupInputChange = (field, value) => {
|
||||
setSignupFormData((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
setIsSignupDirty(true);
|
||||
};
|
||||
|
||||
const handleSignupSave = () => {
|
||||
updateSignupSettingsMutation.mutate({
|
||||
signupEnabled: signupFormData.signupEnabled,
|
||||
defaultUserRole: signupFormData.defaultUserRole,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteUser = async (userId, username) => {
|
||||
if (
|
||||
window.confirm(
|
||||
@@ -474,6 +525,138 @@ const UsersTab = () => {
|
||||
isLoading={resetPasswordMutation.isPending}
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* User Registration Settings */}
|
||||
<div className="bg-white dark:bg-secondary-800 shadow overflow-hidden sm:rounded-lg">
|
||||
<div className="px-6 py-4 border-b border-secondary-200 dark:border-secondary-600">
|
||||
<h3 className="text-lg font-medium text-secondary-900 dark:text-white">
|
||||
User Registration Settings
|
||||
</h3>
|
||||
</div>
|
||||
<div className="px-6 py-4 space-y-4">
|
||||
{/* User Signup Setting */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
id={signupEnabledId}
|
||||
type="checkbox"
|
||||
checked={signupFormData.signupEnabled}
|
||||
onChange={(e) =>
|
||||
handleSignupInputChange("signupEnabled", e.target.checked)
|
||||
}
|
||||
className="rounded border-secondary-300 text-primary-600 shadow-sm focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50"
|
||||
disabled={settingsLoading}
|
||||
/>
|
||||
<label htmlFor={signupEnabledId}>
|
||||
Enable User Self-Registration
|
||||
</label>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Default User Role Dropdown */}
|
||||
{signupFormData.signupEnabled && (
|
||||
<div className="mt-3 ml-6">
|
||||
<label
|
||||
htmlFor={defaultRoleId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
|
||||
>
|
||||
Default Role for New Users
|
||||
</label>
|
||||
<select
|
||||
id={defaultRoleId}
|
||||
value={signupFormData.defaultUserRole}
|
||||
onChange={(e) =>
|
||||
handleSignupInputChange("defaultUserRole", e.target.value)
|
||||
}
|
||||
className="w-full max-w-xs border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
||||
disabled={rolesLoading}
|
||||
>
|
||||
{rolesLoading ? (
|
||||
<option>Loading roles...</option>
|
||||
) : roles && Array.isArray(roles) ? (
|
||||
roles.map((role) => (
|
||||
<option key={role.role} value={role.role}>
|
||||
{role.role.charAt(0).toUpperCase() + role.role.slice(1)}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="user">User</option>
|
||||
)}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||
New users will be assigned this role when they register.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
||||
When enabled, users can create their own accounts through the
|
||||
signup page. When disabled, only administrators can create user
|
||||
accounts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Security Notice */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<Shield className="h-5 w-5 text-blue-400 dark:text-blue-300" />
|
||||
<div className="ml-3">
|
||||
<h3 className="text-sm font-medium text-blue-800 dark:text-blue-200">
|
||||
Security Notice
|
||||
</h3>
|
||||
<p className="mt-1 text-sm text-blue-700 dark:text-blue-300">
|
||||
When enabling user self-registration, exercise caution on
|
||||
internal networks. Consider restricting access to trusted
|
||||
networks only and ensure proper role assignments to prevent
|
||||
unauthorized access to sensitive systems.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
<button
|
||||
type="button"
|
||||
onClick={handleSignupSave}
|
||||
disabled={
|
||||
!isSignupDirty || updateSignupSettingsMutation.isPending
|
||||
}
|
||||
className={`inline-flex items-center px-4 py-2 border border-transparent text-sm font-medium rounded-md shadow-sm text-white ${
|
||||
!isSignupDirty || updateSignupSettingsMutation.isPending
|
||||
? "bg-secondary-400 cursor-not-allowed"
|
||||
: "bg-primary-600 hover:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-primary-500"
|
||||
}`}
|
||||
>
|
||||
{updateSignupSettingsMutation.isPending ? (
|
||||
<>
|
||||
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white mr-2"></div>
|
||||
Saving...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Save className="h-4 w-4 mr-2" />
|
||||
Save Settings
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{updateSignupSettingsMutation.isSuccess && (
|
||||
<div className="bg-green-50 dark:bg-green-900 border border-green-200 dark:border-green-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
<CheckCircle className="h-5 w-5 text-green-400 dark:text-green-300" />
|
||||
<div className="ml-3">
|
||||
<p className="text-sm text-green-700 dark:text-green-300">
|
||||
Settings saved successfully!
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
|
||||
import {
|
||||
AlertCircle,
|
||||
CheckCircle,
|
||||
@@ -8,9 +9,30 @@ import {
|
||||
GitCommit,
|
||||
} from "lucide-react";
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import { versionAPI } from "../../utils/api";
|
||||
import { settingsAPI, versionAPI } from "../../utils/api";
|
||||
|
||||
const VersionUpdateTab = () => {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
// Fetch current settings
|
||||
const { data: settings, isLoading: settingsLoading } = useQuery({
|
||||
queryKey: ["settings"],
|
||||
queryFn: () => settingsAPI.get().then((res) => res.data),
|
||||
});
|
||||
|
||||
// Update settings mutation
|
||||
const updateSettingsMutation = useMutation({
|
||||
mutationFn: (data) => {
|
||||
return settingsAPI.update(data).then((res) => res.data);
|
||||
},
|
||||
onSuccess: () => {
|
||||
queryClient.invalidateQueries(["settings"]);
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error("Failed to update settings:", error);
|
||||
},
|
||||
});
|
||||
|
||||
// Version checking state
|
||||
const [versionInfo, setVersionInfo] = useState({
|
||||
currentVersion: null,
|
||||
@@ -101,6 +123,50 @@ const VersionUpdateTab = () => {
|
||||
)}
|
||||
</p>
|
||||
|
||||
{/* Toggle for showing GitHub version on login */}
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg p-4 border border-secondary-200 dark:border-secondary-600 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex-1">
|
||||
<label
|
||||
htmlFor="show-github-version-toggle"
|
||||
className="text-sm font-medium text-secondary-900 dark:text-white cursor-pointer"
|
||||
>
|
||||
Show GitHub Version / Release Notes on Login Screen
|
||||
</label>
|
||||
<p className="text-xs text-secondary-500 dark:text-secondary-400 mt-1">
|
||||
When enabled, the login screen will display the latest GitHub
|
||||
release version and release notes information.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
id="show-github-version-toggle"
|
||||
onClick={() => {
|
||||
const newValue = !settings?.show_github_version_on_login;
|
||||
updateSettingsMutation.mutate({
|
||||
showGithubVersionOnLogin: newValue,
|
||||
});
|
||||
}}
|
||||
disabled={settingsLoading || updateSettingsMutation.isPending}
|
||||
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
|
||||
settings?.show_github_version_on_login !== false
|
||||
? "bg-primary-600"
|
||||
: "bg-secondary-300 dark:bg-secondary-600"
|
||||
} ${settingsLoading || updateSettingsMutation.isPending ? "opacity-50 cursor-not-allowed" : ""}`}
|
||||
role="switch"
|
||||
aria-checked={settings?.show_github_version_on_login !== false}
|
||||
>
|
||||
<span
|
||||
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
||||
settings?.show_github_version_on_login !== false
|
||||
? "translate-x-5"
|
||||
: "translate-x-0"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{/* My Version */}
|
||||
<div className="bg-white dark:bg-secondary-800 rounded-lg p-4 border border-secondary-200 dark:border-secondary-600">
|
||||
|
||||
@@ -71,7 +71,12 @@ export const AuthProvider = ({ children }) => {
|
||||
if (storedToken && storedUser) {
|
||||
try {
|
||||
setToken(storedToken);
|
||||
setUser(JSON.parse(storedUser));
|
||||
const parsedUser = JSON.parse(storedUser);
|
||||
setUser({
|
||||
...parsedUser,
|
||||
accepted_release_notes_versions:
|
||||
parsedUser.accepted_release_notes_versions || [],
|
||||
});
|
||||
// Fetch permissions from backend
|
||||
fetchPermissions(storedToken);
|
||||
// User is authenticated, skip setup check
|
||||
@@ -128,9 +133,20 @@ export const AuthProvider = ({ children }) => {
|
||||
|
||||
// Regular successful login
|
||||
setToken(data.token);
|
||||
setUser(data.user);
|
||||
setUser({
|
||||
...data.user,
|
||||
accepted_release_notes_versions:
|
||||
data.user.accepted_release_notes_versions || [],
|
||||
});
|
||||
localStorage.setItem("token", data.token);
|
||||
localStorage.setItem("user", JSON.stringify(data.user));
|
||||
localStorage.setItem(
|
||||
"user",
|
||||
JSON.stringify({
|
||||
...data.user,
|
||||
accepted_release_notes_versions:
|
||||
data.user.accepted_release_notes_versions || [],
|
||||
}),
|
||||
);
|
||||
|
||||
// Fetch user permissions after successful login
|
||||
const userPermissions = await fetchPermissions(data.token);
|
||||
@@ -237,8 +253,19 @@ export const AuthProvider = ({ children }) => {
|
||||
}
|
||||
|
||||
// Update both state and localStorage atomically
|
||||
setUser(data.user);
|
||||
localStorage.setItem("user", JSON.stringify(data.user));
|
||||
setUser({
|
||||
...data.user,
|
||||
accepted_release_notes_versions:
|
||||
data.user.accepted_release_notes_versions || [],
|
||||
});
|
||||
localStorage.setItem(
|
||||
"user",
|
||||
JSON.stringify({
|
||||
...data.user,
|
||||
accepted_release_notes_versions:
|
||||
data.user.accepted_release_notes_versions || [],
|
||||
}),
|
||||
);
|
||||
|
||||
return { success: true, user: data.user };
|
||||
} else {
|
||||
@@ -349,6 +376,48 @@ export const AuthProvider = ({ children }) => {
|
||||
}
|
||||
};
|
||||
|
||||
const acceptReleaseNotes = async (version) => {
|
||||
try {
|
||||
const response = await fetch("/api/v1/release-notes-acceptance/accept", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({ version }),
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (response.ok) {
|
||||
// Update user state immediately with new accepted version
|
||||
const updatedAcceptedVersions = [
|
||||
...(user?.accepted_release_notes_versions || []),
|
||||
version,
|
||||
];
|
||||
|
||||
const updatedUser = {
|
||||
...user,
|
||||
accepted_release_notes_versions: updatedAcceptedVersions,
|
||||
};
|
||||
|
||||
// Update both state and localStorage atomically
|
||||
setUser(updatedUser);
|
||||
localStorage.setItem("user", JSON.stringify(updatedUser));
|
||||
|
||||
return { success: true };
|
||||
} else {
|
||||
return {
|
||||
success: false,
|
||||
error: data.error || "Failed to accept release notes",
|
||||
};
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error accepting release notes:", error);
|
||||
return { success: false, error: "Network error occurred" };
|
||||
}
|
||||
};
|
||||
|
||||
const isAdmin = () => {
|
||||
return user?.role === "admin";
|
||||
};
|
||||
@@ -411,14 +480,25 @@ export const AuthProvider = ({ children }) => {
|
||||
// Use flushSync to ensure all state updates are applied synchronously
|
||||
flushSync(() => {
|
||||
setToken(authToken);
|
||||
setUser(authUser);
|
||||
setUser({
|
||||
...authUser,
|
||||
accepted_release_notes_versions:
|
||||
authUser.accepted_release_notes_versions || [],
|
||||
});
|
||||
setNeedsFirstTimeSetup(false);
|
||||
setAuthPhase(AUTH_PHASES.READY);
|
||||
});
|
||||
|
||||
// Store in localStorage after state is updated
|
||||
localStorage.setItem("token", authToken);
|
||||
localStorage.setItem("user", JSON.stringify(authUser));
|
||||
localStorage.setItem(
|
||||
"user",
|
||||
JSON.stringify({
|
||||
...authUser,
|
||||
accepted_release_notes_versions:
|
||||
authUser.accepted_release_notes_versions || [],
|
||||
}),
|
||||
);
|
||||
|
||||
// Fetch permissions immediately for the new authenticated user
|
||||
fetchPermissions(authToken);
|
||||
@@ -458,6 +538,7 @@ export const AuthProvider = ({ children }) => {
|
||||
canViewReports,
|
||||
canExportData,
|
||||
canManageSettings,
|
||||
acceptReleaseNotes,
|
||||
};
|
||||
|
||||
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
|
||||
|
||||
@@ -49,6 +49,8 @@ const Login = () => {
|
||||
const [requiresTfa, setRequiresTfa] = useState(false);
|
||||
const [tfaUsername, setTfaUsername] = useState("");
|
||||
const [signupEnabled, setSignupEnabled] = useState(false);
|
||||
const [showGithubVersionOnLogin, setShowGithubVersionOnLogin] =
|
||||
useState(true);
|
||||
const [latestRelease, setLatestRelease] = useState(null);
|
||||
const [githubStars, setGithubStars] = useState(null);
|
||||
const canvasRef = useRef(null);
|
||||
@@ -151,26 +153,35 @@ const Login = () => {
|
||||
return () => window.removeEventListener("resize", handleResize);
|
||||
}, [themeConfig]);
|
||||
|
||||
// Check if signup is enabled
|
||||
// Check login settings (signup enabled and show github version)
|
||||
useEffect(() => {
|
||||
const checkSignupEnabled = async () => {
|
||||
const checkLoginSettings = async () => {
|
||||
try {
|
||||
const response = await fetch("/api/v1/auth/signup-enabled");
|
||||
const response = await fetch("/api/v1/settings/login-settings");
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
setSignupEnabled(data.signupEnabled);
|
||||
setSignupEnabled(data.signup_enabled || false);
|
||||
setShowGithubVersionOnLogin(
|
||||
data.show_github_version_on_login !== false,
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Failed to check signup status:", error);
|
||||
console.error("Failed to check login settings:", error);
|
||||
// Default to disabled on error for security
|
||||
setSignupEnabled(false);
|
||||
setShowGithubVersionOnLogin(true); // Default to showing on error
|
||||
}
|
||||
};
|
||||
checkSignupEnabled();
|
||||
checkLoginSettings();
|
||||
}, []);
|
||||
|
||||
// Fetch latest release and stars from GitHub
|
||||
useEffect(() => {
|
||||
// Only fetch if the setting allows it
|
||||
if (!showGithubVersionOnLogin) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fetchGitHubData = async () => {
|
||||
try {
|
||||
// Try to get cached data first
|
||||
@@ -261,7 +272,7 @@ const Login = () => {
|
||||
};
|
||||
|
||||
fetchGitHubData();
|
||||
}, []); // Run once on mount
|
||||
}, [showGithubVersionOnLogin]); // Run once on mount
|
||||
|
||||
const handleSubmit = async (e) => {
|
||||
e.preventDefault();
|
||||
@@ -449,209 +460,213 @@ const Login = () => {
|
||||
<canvas ref={canvasRef} className="absolute inset-0 w-full h-full" />
|
||||
<div className="absolute inset-0 bg-gradient-to-br from-black/40 to-black/60" />
|
||||
|
||||
{/* Left side - Info Panel (hidden on mobile) */}
|
||||
<div className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative z-10">
|
||||
<div className="flex flex-col justify-between text-white p-12 h-full w-full">
|
||||
<div className="flex-1 flex flex-col justify-center items-start max-w-xl mx-auto">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<img
|
||||
src="/assets/logo_dark.png"
|
||||
alt="PatchMon"
|
||||
className="h-16 mb-4"
|
||||
/>
|
||||
<p className="text-sm text-blue-200 font-medium tracking-wide uppercase">
|
||||
Linux Patch Monitoring
|
||||
</p>
|
||||
</div>
|
||||
{/* Left side - Info Panel (hidden on mobile or when GitHub version is disabled) */}
|
||||
{showGithubVersionOnLogin && (
|
||||
<div className="hidden lg:flex lg:w-1/2 xl:w-3/5 relative z-10">
|
||||
<div className="flex flex-col justify-between text-white p-12 h-full w-full">
|
||||
<div className="flex-1 flex flex-col justify-center items-start max-w-xl mx-auto">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<img
|
||||
src="/assets/logo_dark.png"
|
||||
alt="PatchMon"
|
||||
className="h-16 mb-4"
|
||||
/>
|
||||
<p className="text-sm text-blue-200 font-medium tracking-wide uppercase">
|
||||
Linux Patch Monitoring
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{latestRelease ? (
|
||||
<div className="space-y-4 bg-black/20 backdrop-blur-sm rounded-lg p-6 border border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
|
||||
<span className="text-green-300 text-sm font-semibold">
|
||||
Latest Release
|
||||
{showGithubVersionOnLogin && latestRelease ? (
|
||||
<div className="space-y-4 bg-black/20 backdrop-blur-sm rounded-lg p-6 border border-white/10">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 bg-green-400 rounded-full animate-pulse" />
|
||||
<span className="text-green-300 text-sm font-semibold">
|
||||
Latest Release
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{latestRelease.version}
|
||||
</span>
|
||||
</div>
|
||||
<span className="text-2xl font-bold text-white">
|
||||
{latestRelease.version}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{latestRelease.name && (
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
{latestRelease.name}
|
||||
</h3>
|
||||
)}
|
||||
{latestRelease.name && (
|
||||
<h3 className="text-lg font-semibold text-white">
|
||||
{latestRelease.name}
|
||||
</h3>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-sm text-gray-300">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="Release date"
|
||||
<div className="flex items-center gap-2 text-sm text-gray-300">
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="Release date"
|
||||
>
|
||||
<title>Release date</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Released {latestRelease.publishedAt}</span>
|
||||
</div>
|
||||
|
||||
{latestRelease.body && (
|
||||
<p className="text-sm text-gray-300 leading-relaxed line-clamp-3">
|
||||
{latestRelease.body}
|
||||
</p>
|
||||
)}
|
||||
|
||||
<a
|
||||
href="https://github.com/PatchMon/PatchMon/releases/latest"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm text-blue-300 hover:text-blue-200 transition-colors font-medium"
|
||||
>
|
||||
<title>Release date</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"
|
||||
/>
|
||||
</svg>
|
||||
<span>Released {latestRelease.publishedAt}</span>
|
||||
View Release Notes
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="External link"
|
||||
>
|
||||
<title>External link</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
</a>
|
||||
</div>
|
||||
) : showGithubVersionOnLogin ? (
|
||||
<div className="space-y-4 bg-black/20 backdrop-blur-sm rounded-lg p-6 border border-white/10">
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-6 bg-white/20 rounded w-3/4" />
|
||||
<div className="h-4 bg-white/20 rounded w-1/2" />
|
||||
<div className="h-4 bg-white/20 rounded w-full" />
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{latestRelease.body && (
|
||||
<p className="text-sm text-gray-300 leading-relaxed line-clamp-3">
|
||||
{latestRelease.body}
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* Social Links Footer */}
|
||||
<div className="max-w-xl mx-auto w-full">
|
||||
<div className="border-t border-white/10 pt-6">
|
||||
<p className="text-sm text-gray-400 mb-4">Connect with us</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* GitHub */}
|
||||
<a
|
||||
href="https://github.com/PatchMon/PatchMon/releases/latest"
|
||||
href="https://github.com/PatchMon/PatchMon"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="inline-flex items-center gap-2 text-sm text-blue-300 hover:text-blue-200 transition-colors font-medium"
|
||||
className="flex items-center justify-center gap-1.5 px-3 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="GitHub Repository"
|
||||
>
|
||||
View Release Notes
|
||||
<svg
|
||||
className="w-4 h-4"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="External link"
|
||||
>
|
||||
<title>External link</title>
|
||||
<path
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth={2}
|
||||
d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14"
|
||||
/>
|
||||
</svg>
|
||||
<Github className="h-5 w-5 text-white" />
|
||||
{githubStars !== null && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-3.5 w-3.5 fill-current text-yellow-400" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
{githubStars}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
|
||||
{/* Roadmap */}
|
||||
<a
|
||||
href="https://github.com/orgs/PatchMon/projects/2/views/1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Roadmap"
|
||||
>
|
||||
<Route className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Docs */}
|
||||
<a
|
||||
href="https://docs.patchmon.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Documentation"
|
||||
>
|
||||
<BookOpen className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Discord */}
|
||||
<a
|
||||
href="https://patchmon.net/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Discord Community"
|
||||
>
|
||||
<DiscordIcon className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Email */}
|
||||
<a
|
||||
href="mailto:support@patchmon.net"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Email Support"
|
||||
>
|
||||
<Mail className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* YouTube */}
|
||||
<a
|
||||
href="https://youtube.com/@patchmonTV"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="YouTube Channel"
|
||||
>
|
||||
<FaYoutube className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Reddit */}
|
||||
<a
|
||||
href="https://www.reddit.com/r/patchmon"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Reddit Community"
|
||||
>
|
||||
<FaReddit className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Website */}
|
||||
<a
|
||||
href="https://patchmon.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Visit patchmon.net"
|
||||
>
|
||||
<Globe className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4 bg-black/20 backdrop-blur-sm rounded-lg p-6 border border-white/10">
|
||||
<div className="animate-pulse space-y-3">
|
||||
<div className="h-6 bg-white/20 rounded w-3/4" />
|
||||
<div className="h-4 bg-white/20 rounded w-1/2" />
|
||||
<div className="h-4 bg-white/20 rounded w-full" />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Social Links Footer */}
|
||||
<div className="max-w-xl mx-auto w-full">
|
||||
<div className="border-t border-white/10 pt-6">
|
||||
<p className="text-sm text-gray-400 mb-4">Connect with us</p>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
{/* GitHub */}
|
||||
<a
|
||||
href="https://github.com/PatchMon/PatchMon"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center gap-1.5 px-3 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="GitHub Repository"
|
||||
>
|
||||
<Github className="h-5 w-5 text-white" />
|
||||
{githubStars !== null && (
|
||||
<div className="flex items-center gap-1">
|
||||
<Star className="h-3.5 w-3.5 fill-current text-yellow-400" />
|
||||
<span className="text-sm font-medium text-white">
|
||||
{githubStars}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</a>
|
||||
|
||||
{/* Roadmap */}
|
||||
<a
|
||||
href="https://github.com/orgs/PatchMon/projects/2/views/1"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Roadmap"
|
||||
>
|
||||
<Route className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Docs */}
|
||||
<a
|
||||
href="https://docs.patchmon.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Documentation"
|
||||
>
|
||||
<BookOpen className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Discord */}
|
||||
<a
|
||||
href="https://patchmon.net/discord"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Discord Community"
|
||||
>
|
||||
<DiscordIcon className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Email */}
|
||||
<a
|
||||
href="mailto:support@patchmon.net"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Email Support"
|
||||
>
|
||||
<Mail className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* YouTube */}
|
||||
<a
|
||||
href="https://youtube.com/@patchmonTV"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="YouTube Channel"
|
||||
>
|
||||
<FaYoutube className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Reddit */}
|
||||
<a
|
||||
href="https://www.reddit.com/r/patchmon"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Reddit Community"
|
||||
>
|
||||
<FaReddit className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
|
||||
{/* Website */}
|
||||
<a
|
||||
href="https://patchmon.net"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center justify-center w-10 h-10 bg-white/10 hover:bg-white/20 backdrop-blur-sm rounded-lg transition-colors border border-white/10"
|
||||
title="Visit patchmon.net"
|
||||
>
|
||||
<Globe className="h-5 w-5 text-white" />
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Right side - Login Form */}
|
||||
<div className="flex-1 flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative z-10">
|
||||
<div
|
||||
className={`${showGithubVersionOnLogin ? "flex-1" : "w-full"} flex items-center justify-center py-12 px-4 sm:px-6 lg:px-8 relative z-10`}
|
||||
>
|
||||
<div className="max-w-md w-full space-y-8 bg-white dark:bg-secondary-900 rounded-2xl shadow-2xl p-8 lg:p-10">
|
||||
<div>
|
||||
<div className="mx-auto h-16 w-16 flex items-center justify-center">
|
||||
|
||||
@@ -18,12 +18,7 @@ import {
|
||||
import { useEffect, useId, useState } from "react";
|
||||
import UpgradeNotificationIcon from "../components/UpgradeNotificationIcon";
|
||||
import { useUpdateNotification } from "../contexts/UpdateNotificationContext";
|
||||
import {
|
||||
agentFileAPI,
|
||||
permissionsAPI,
|
||||
settingsAPI,
|
||||
versionAPI,
|
||||
} from "../utils/api";
|
||||
import { agentFileAPI, settingsAPI, versionAPI } from "../utils/api";
|
||||
|
||||
const Settings = () => {
|
||||
const repoPublicId = useId();
|
||||
@@ -33,7 +28,6 @@ const Settings = () => {
|
||||
const hostId = useId();
|
||||
const portId = useId();
|
||||
const updateIntervalId = useId();
|
||||
const defaultRoleId = useId();
|
||||
const githubRepoUrlId = useId();
|
||||
const sshKeyPathId = useId();
|
||||
const _scriptFileId = useId();
|
||||
@@ -44,8 +38,6 @@ const Settings = () => {
|
||||
serverPort: 3001,
|
||||
updateInterval: 60,
|
||||
autoUpdate: false,
|
||||
signupEnabled: false,
|
||||
defaultUserRole: "user",
|
||||
githubRepoUrl: "git@github.com:9technologygroup/patchmon.net.git",
|
||||
repositoryType: "public",
|
||||
sshKeyPath: "",
|
||||
@@ -175,12 +167,6 @@ const Settings = () => {
|
||||
return settings?.ignore_ssl_self_signed ? "-sk" : "-s";
|
||||
};
|
||||
|
||||
// Fetch available roles for default user role dropdown
|
||||
const { data: roles, isLoading: rolesLoading } = useQuery({
|
||||
queryKey: ["rolePermissions"],
|
||||
queryFn: () => permissionsAPI.getRoles().then((res) => res.data),
|
||||
});
|
||||
|
||||
// Update form data when settings are loaded
|
||||
useEffect(() => {
|
||||
if (settings) {
|
||||
@@ -190,9 +176,6 @@ const Settings = () => {
|
||||
serverPort: settings.server_port || 3001,
|
||||
updateInterval: settings.update_interval || 60,
|
||||
autoUpdate: settings.auto_update || false,
|
||||
// biome-ignore lint/complexity/noUselessTernary: Seems to be desired given the comment
|
||||
signupEnabled: settings.signup_enabled === true ? true : false, // Explicit boolean conversion
|
||||
defaultUserRole: settings.default_user_role || "user",
|
||||
githubRepoUrl:
|
||||
settings.github_repo_url ||
|
||||
"https://github.com/PatchMon/PatchMon.git",
|
||||
@@ -997,66 +980,6 @@ const Settings = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* User Signup Setting */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={formData.signupEnabled}
|
||||
onChange={(e) =>
|
||||
handleInputChange("signupEnabled", e.target.checked)
|
||||
}
|
||||
className="rounded border-secondary-300 text-primary-600 shadow-sm focus:border-primary-300 focus:ring focus:ring-primary-200 focus:ring-opacity-50"
|
||||
/>
|
||||
Enable User Self-Registration
|
||||
</div>
|
||||
</label>
|
||||
|
||||
{/* Default User Role Dropdown */}
|
||||
{formData.signupEnabled && (
|
||||
<div className="mt-3 ml-6">
|
||||
<label
|
||||
htmlFor={defaultRoleId}
|
||||
className="block text-sm font-medium text-secondary-700 dark:text-secondary-200 mb-2"
|
||||
>
|
||||
Default Role for New Users
|
||||
</label>
|
||||
<select
|
||||
id={defaultRoleId}
|
||||
value={formData.defaultUserRole}
|
||||
onChange={(e) =>
|
||||
handleInputChange("defaultUserRole", e.target.value)
|
||||
}
|
||||
className="w-full max-w-xs border-secondary-300 dark:border-secondary-600 rounded-md shadow-sm focus:ring-primary-500 focus:border-primary-500 bg-white dark:bg-secondary-700 text-secondary-900 dark:text-white"
|
||||
disabled={rolesLoading}
|
||||
>
|
||||
{rolesLoading ? (
|
||||
<option>Loading roles...</option>
|
||||
) : roles && Array.isArray(roles) ? (
|
||||
roles.map((role) => (
|
||||
<option key={role.role} value={role.role}>
|
||||
{role.role.charAt(0).toUpperCase() +
|
||||
role.role.slice(1)}
|
||||
</option>
|
||||
))
|
||||
) : (
|
||||
<option value="user">User</option>
|
||||
)}
|
||||
</select>
|
||||
<p className="mt-1 text-xs text-secondary-500 dark:text-secondary-400">
|
||||
New users will be assigned this role when they register.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="mt-1 text-sm text-secondary-500 dark:text-secondary-400">
|
||||
When enabled, users can create their own accounts through the
|
||||
signup page. When disabled, only administrators can create
|
||||
user accounts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Security Notice */}
|
||||
<div className="bg-blue-50 dark:bg-blue-900 border border-blue-200 dark:border-blue-700 rounded-md p-4">
|
||||
<div className="flex">
|
||||
|
||||
1733
package-lock.json
generated
1733
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user