Made github version checking better

Added functionality of Logo branding
Modified sidebar width
This commit is contained in:
Muhammad Ibrahim
2025-10-05 10:55:34 +01:00
parent 3ea8cc74b6
commit 6988ecab12
19 changed files with 1373 additions and 146 deletions

View File

@@ -0,0 +1,4 @@
-- AddLogoFieldsToSettings
ALTER TABLE "settings" ADD COLUMN "logo_dark" VARCHAR(255) DEFAULT '/assets/logo_dark.png';
ALTER TABLE "settings" ADD COLUMN "logo_light" VARCHAR(255) DEFAULT '/assets/logo_light.png';
ALTER TABLE "settings" ADD COLUMN "favicon" VARCHAR(255) DEFAULT '/assets/logo_square.svg';

View File

@@ -164,6 +164,9 @@ model settings {
signup_enabled Boolean @default(false)
default_user_role String @default("user")
ignore_ssl_self_signed Boolean @default(false)
logo_dark String? @default("/assets/logo_dark.png")
logo_light String? @default("/assets/logo_light.png")
favicon String? @default("/assets/logo_square.svg")
}
model update_history {

View File

@@ -215,6 +215,18 @@ router.put(
}
return true;
}),
body("logoDark")
.optional()
.isLength({ min: 1 })
.withMessage("Logo dark path must be a non-empty string"),
body("logoLight")
.optional()
.isLength({ min: 1 })
.withMessage("Logo light path must be a non-empty string"),
body("favicon")
.optional()
.isLength({ min: 1 })
.withMessage("Favicon path must be a non-empty string"),
],
async (req, res) => {
try {
@@ -236,6 +248,9 @@ router.put(
githubRepoUrl,
repositoryType,
sshKeyPath,
logoDark,
logoLight,
favicon,
} = req.body;
// Get current settings to check for update interval changes
@@ -264,6 +279,9 @@ router.put(
if (repositoryType !== undefined)
updateData.repository_type = repositoryType;
if (sshKeyPath !== undefined) updateData.ssh_key_path = sshKeyPath;
if (logoDark !== undefined) updateData.logo_dark = logoDark;
if (logoLight !== undefined) updateData.logo_light = logoLight;
if (favicon !== undefined) updateData.favicon = favicon;
const updatedSettings = await updateSettings(
currentSettings.id,
@@ -351,4 +369,175 @@ router.get("/auto-update", async (_req, res) => {
}
});
// Upload logo files
router.post(
"/logos/upload",
authenticateToken,
requireManageSettings,
async (req, res) => {
try {
const { logoType, fileContent, fileName } = req.body;
if (!logoType || !fileContent) {
return res.status(400).json({
error: "Logo type and file content are required",
});
}
if (!["dark", "light", "favicon"].includes(logoType)) {
return res.status(400).json({
error: "Logo type must be 'dark', 'light', or 'favicon'",
});
}
// Validate file content (basic checks)
if (typeof fileContent !== "string") {
return res.status(400).json({
error: "File content must be a base64 string",
});
}
const fs = require("node:fs").promises;
const path = require("node:path");
const _crypto = require("node:crypto");
// Create assets directory if it doesn't exist
// In development: save to public/assets (served by Vite)
// In production: save to dist/assets (served by built app)
const isDevelopment = process.env.NODE_ENV !== "production";
const assetsDir = isDevelopment
? path.join(__dirname, "../../../frontend/public/assets")
: path.join(__dirname, "../../../frontend/dist/assets");
await fs.mkdir(assetsDir, { recursive: true });
// Determine file extension and path
let fileExtension;
let fileName_final;
if (logoType === "favicon") {
fileExtension = ".svg";
fileName_final = fileName || "logo_square.svg";
} else {
// Determine extension from file content or use default
if (fileContent.startsWith("data:image/png")) {
fileExtension = ".png";
} else if (fileContent.startsWith("data:image/svg")) {
fileExtension = ".svg";
} else if (
fileContent.startsWith("data:image/jpeg") ||
fileContent.startsWith("data:image/jpg")
) {
fileExtension = ".jpg";
} else {
fileExtension = ".png"; // Default to PNG
}
fileName_final = fileName || `logo_${logoType}${fileExtension}`;
}
const filePath = path.join(assetsDir, fileName_final);
// Handle base64 data URLs
let fileBuffer;
if (fileContent.startsWith("data:")) {
const base64Data = fileContent.split(",")[1];
fileBuffer = Buffer.from(base64Data, "base64");
} else {
// Assume it's already base64
fileBuffer = Buffer.from(fileContent, "base64");
}
// Create backup of existing file
try {
const backupPath = `${filePath}.backup.${Date.now()}`;
await fs.copyFile(filePath, backupPath);
console.log(`Created backup: ${backupPath}`);
} catch (error) {
// Ignore if original doesn't exist
if (error.code !== "ENOENT") {
console.warn("Failed to create backup:", error.message);
}
}
// Write new logo file
await fs.writeFile(filePath, fileBuffer);
// Update settings with new logo path
const settings = await getSettings();
const logoPath = `/assets/${fileName_final}`;
const updateData = {};
if (logoType === "dark") {
updateData.logo_dark = logoPath;
} else if (logoType === "light") {
updateData.logo_light = logoPath;
} else if (logoType === "favicon") {
updateData.favicon = logoPath;
}
await updateSettings(settings.id, updateData);
// Get file stats
const stats = await fs.stat(filePath);
res.json({
message: `${logoType} logo uploaded successfully`,
fileName: fileName_final,
path: logoPath,
size: stats.size,
sizeFormatted: `${(stats.size / 1024).toFixed(1)} KB`,
});
} catch (error) {
console.error("Upload logo error:", error);
res.status(500).json({ error: "Failed to upload logo" });
}
},
);
// Reset logo to default
router.post(
"/logos/reset",
authenticateToken,
requireManageSettings,
async (req, res) => {
try {
const { logoType } = req.body;
if (!logoType) {
return res.status(400).json({
error: "Logo type is required",
});
}
if (!["dark", "light", "favicon"].includes(logoType)) {
return res.status(400).json({
error: "Logo type must be 'dark', 'light', or 'favicon'",
});
}
// Get current settings
const settings = await getSettings();
// Clear the custom logo path to revert to default
const updateData = {};
if (logoType === "dark") {
updateData.logo_dark = null;
} else if (logoType === "light") {
updateData.logo_light = null;
} else if (logoType === "favicon") {
updateData.favicon = null;
}
await updateSettings(settings.id, updateData);
res.json({
message: `${logoType} logo reset to default successfully`,
logoType,
});
} catch (error) {
console.error("Reset logo error:", error);
res.status(500).json({ error: "Failed to reset logo" });
}
},
);
module.exports = router;

View File

@@ -188,72 +188,24 @@ router.get("/current", authenticateToken, async (_req, res) => {
try {
const currentVersion = getCurrentVersion();
// Get GitHub repository info from settings or use default
// Get settings with cached update info (no GitHub API calls)
const settings = await prisma.settings.findFirst();
const githubRepoUrl = settings?.githubRepoUrl || DEFAULT_GITHUB_REPO;
const { owner, repo } = parseGitHubRepo(githubRepoUrl);
let latestRelease = null;
let latestCommit = null;
let commitDifference = null;
// Fetch GitHub data if we have valid owner/repo
if (owner && repo) {
try {
// Fetch latest release, latest commit, and commit difference in parallel
const [releaseData, commitData, differenceData] = await Promise.all([
getLatestRelease(owner, repo),
getLatestCommit(owner, repo),
getCommitDifference(owner, repo, currentVersion),
]);
latestRelease = releaseData;
latestCommit = commitData;
commitDifference = differenceData;
} catch (githubError) {
console.warn("Failed to fetch GitHub data:", githubError.message);
// Provide fallback data when GitHub API is rate-limited
if (
githubError.message.includes("rate limit") ||
githubError.message.includes("API rate limit")
) {
console.log("GitHub API rate limited, providing fallback data");
latestRelease = {
tagName: "v1.2.7",
version: "1.2.7",
publishedAt: "2025-10-02T17:12:53Z",
htmlUrl: "https://github.com/PatchMon/PatchMon/releases/tag/v1.2.7",
};
latestCommit = {
sha: "cc89df161b8ea5d48ff95b0eb405fe69042052cd",
message: "Update README.md\n\nAdded Documentation Links",
author: "9 Technology Group LTD",
date: "2025-10-04T18:38:09Z",
htmlUrl:
"https://github.com/PatchMon/PatchMon/commit/cc89df161b8ea5d48ff95b0eb405fe69042052cd",
};
commitDifference = {
commitsBehind: 0,
commitsAhead: 3, // Main branch is ahead of release
totalCommits: 3,
branchInfo: "main branch vs release",
};
}
}
}
// Return current version and cached update information
// The backend scheduler updates this data periodically
res.json({
version: currentVersion,
latest_version: settings?.latest_version || null,
is_update_available: settings?.is_update_available || false,
last_update_check: settings?.last_update_check || null,
buildDate: new Date().toISOString(),
environment: process.env.NODE_ENV || "development",
github: {
repository: githubRepoUrl,
owner: owner,
repo: repo,
latestRelease: latestRelease,
latestCommit: latestCommit,
commitDifference: commitDifference,
},
});
} catch (error) {

View File

@@ -1,5 +1,5 @@
const jwt = require("jsonwebtoken");
const crypto = require("crypto");
const crypto = require("node:crypto");
const { PrismaClient } = require("@prisma/client");
const prisma = new PrismaClient();