mirror of
https://github.com/9technologygroup/patchmon.net.git
synced 2025-11-11 17:36:04 +00:00
api endpoint and scopes created
This commit is contained in:
@@ -288,6 +288,7 @@ model auto_enrollment_tokens {
|
|||||||
last_used_at DateTime?
|
last_used_at DateTime?
|
||||||
expires_at DateTime?
|
expires_at DateTime?
|
||||||
metadata Json?
|
metadata Json?
|
||||||
|
scopes Json?
|
||||||
users users? @relation(fields: [created_by_user_id], references: [id], onDelete: SetNull)
|
users users? @relation(fields: [created_by_user_id], references: [id], onDelete: SetNull)
|
||||||
host_groups host_groups? @relation(fields: [default_host_group_id], references: [id], onDelete: SetNull)
|
host_groups host_groups? @relation(fields: [default_host_group_id], references: [id], onDelete: SetNull)
|
||||||
|
|
||||||
|
|||||||
113
backend/src/middleware/apiAuth.js
Normal file
113
backend/src/middleware/apiAuth.js
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
const { getPrismaClient } = require("../config/prisma");
|
||||||
|
const bcrypt = require("bcryptjs");
|
||||||
|
|
||||||
|
const prisma = getPrismaClient();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Middleware factory to authenticate API tokens using Basic Auth
|
||||||
|
* @param {string} integrationType - The expected integration type (e.g., "api", "gethomepage")
|
||||||
|
* @returns {Function} Express middleware function
|
||||||
|
*/
|
||||||
|
const authenticateApiToken = (integrationType) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
|
||||||
|
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
||||||
|
return res
|
||||||
|
.status(401)
|
||||||
|
.json({ error: "Missing or invalid authorization header" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode base64 credentials
|
||||||
|
const base64Credentials = authHeader.split(" ")[1];
|
||||||
|
const credentials = Buffer.from(base64Credentials, "base64").toString(
|
||||||
|
"ascii",
|
||||||
|
);
|
||||||
|
const [apiKey, apiSecret] = credentials.split(":");
|
||||||
|
|
||||||
|
if (!apiKey || !apiSecret) {
|
||||||
|
return res.status(401).json({ error: "Invalid credentials format" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the token in database
|
||||||
|
const token = await prisma.auto_enrollment_tokens.findUnique({
|
||||||
|
where: { token_key: apiKey },
|
||||||
|
include: {
|
||||||
|
users: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
username: true,
|
||||||
|
role: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!token) {
|
||||||
|
console.log(`API key not found: ${apiKey}`);
|
||||||
|
return res.status(401).json({ error: "Invalid API key" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is active
|
||||||
|
if (!token.is_active) {
|
||||||
|
return res.status(401).json({ error: "API key is disabled" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token has expired
|
||||||
|
if (token.expires_at && new Date(token.expires_at) < new Date()) {
|
||||||
|
return res.status(401).json({ error: "API key has expired" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token is for the expected integration type
|
||||||
|
if (token.metadata?.integration_type !== integrationType) {
|
||||||
|
return res.status(401).json({ error: "Invalid API key type" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the secret
|
||||||
|
const isValidSecret = await bcrypt.compare(apiSecret, token.token_secret);
|
||||||
|
if (!isValidSecret) {
|
||||||
|
return res.status(401).json({ error: "Invalid API secret" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check IP restrictions if any
|
||||||
|
if (token.allowed_ip_ranges && token.allowed_ip_ranges.length > 0) {
|
||||||
|
const clientIp = req.ip || req.connection.remoteAddress;
|
||||||
|
const forwardedFor = req.headers["x-forwarded-for"];
|
||||||
|
const realIp = req.headers["x-real-ip"];
|
||||||
|
|
||||||
|
// Get the actual client IP (considering proxies)
|
||||||
|
const actualClientIp = forwardedFor
|
||||||
|
? forwardedFor.split(",")[0].trim()
|
||||||
|
: realIp || clientIp;
|
||||||
|
|
||||||
|
const isAllowedIp = token.allowed_ip_ranges.some((range) => {
|
||||||
|
// Simple IP range check (can be enhanced for CIDR support)
|
||||||
|
return actualClientIp.startsWith(range) || actualClientIp === range;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isAllowedIp) {
|
||||||
|
console.log(
|
||||||
|
`IP validation failed. Client IP: ${actualClientIp}, Allowed ranges: ${token.allowed_ip_ranges.join(", ")}`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({ error: "IP address not allowed" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update last used timestamp
|
||||||
|
await prisma.auto_enrollment_tokens.update({
|
||||||
|
where: { id: token.id },
|
||||||
|
data: { last_used_at: new Date() },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Attach token info to request
|
||||||
|
req.apiToken = token;
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("API key authentication error:", error);
|
||||||
|
res.status(500).json({ error: "Authentication failed" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { authenticateApiToken };
|
||||||
76
backend/src/middleware/apiScope.js
Normal file
76
backend/src/middleware/apiScope.js
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
/**
|
||||||
|
* Middleware factory to validate API token scopes
|
||||||
|
* Only applies to tokens with metadata.integration_type === "api"
|
||||||
|
* @param {string} resource - The resource being accessed (e.g., "host")
|
||||||
|
* @param {string} action - The action being performed (e.g., "get", "put", "patch", "update", "delete")
|
||||||
|
* @returns {Function} Express middleware function
|
||||||
|
*/
|
||||||
|
const requireApiScope = (resource, action) => {
|
||||||
|
return async (req, res, next) => {
|
||||||
|
try {
|
||||||
|
const token = req.apiToken;
|
||||||
|
|
||||||
|
// If no token attached, this should have been caught by auth middleware
|
||||||
|
if (!token) {
|
||||||
|
return res.status(401).json({ error: "Unauthorized" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only validate scopes for API type tokens
|
||||||
|
if (token.metadata?.integration_type !== "api") {
|
||||||
|
// For non-API tokens, skip scope validation
|
||||||
|
return next();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if token has scopes field
|
||||||
|
if (!token.scopes || typeof token.scopes !== "object") {
|
||||||
|
console.warn(
|
||||||
|
`API token ${token.token_key} missing scopes field for ${resource}:${action}`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({
|
||||||
|
error: "Access denied",
|
||||||
|
message: "This API key does not have the required permissions",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if resource exists in scopes
|
||||||
|
if (!token.scopes[resource]) {
|
||||||
|
console.warn(
|
||||||
|
`API token ${token.token_key} missing resource ${resource} for ${action}`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({
|
||||||
|
error: "Access denied",
|
||||||
|
message: `This API key does not have access to ${resource}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if action exists in resource scopes
|
||||||
|
if (!Array.isArray(token.scopes[resource])) {
|
||||||
|
console.warn(
|
||||||
|
`API token ${token.token_key} has invalid scopes structure for ${resource}`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({
|
||||||
|
error: "Access denied",
|
||||||
|
message: "Invalid API key permissions configuration",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!token.scopes[resource].includes(action)) {
|
||||||
|
console.warn(
|
||||||
|
`API token ${token.token_key} missing action ${action} for resource ${resource}`,
|
||||||
|
);
|
||||||
|
return res.status(403).json({
|
||||||
|
error: "Access denied",
|
||||||
|
message: `This API key does not have permission to ${action} ${resource}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scope validation passed
|
||||||
|
next();
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Scope validation error:", error);
|
||||||
|
res.status(500).json({ error: "Scope validation failed" });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
module.exports = { requireApiScope };
|
||||||
143
backend/src/routes/apiHostsRoutes.js
Normal file
143
backend/src/routes/apiHostsRoutes.js
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
const express = require("express");
|
||||||
|
const { getPrismaClient } = require("../config/prisma");
|
||||||
|
const { authenticateApiToken } = require("../middleware/apiAuth");
|
||||||
|
const { requireApiScope } = require("../middleware/apiScope");
|
||||||
|
|
||||||
|
const router = express.Router();
|
||||||
|
const prisma = getPrismaClient();
|
||||||
|
|
||||||
|
// Helper function to check if a string is a valid UUID
|
||||||
|
const isUUID = (str) => {
|
||||||
|
const uuidRegex =
|
||||||
|
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
||||||
|
return uuidRegex.test(str);
|
||||||
|
};
|
||||||
|
|
||||||
|
// GET /api/v1/api/hosts - List hosts with IP and groups
|
||||||
|
router.get(
|
||||||
|
"/hosts",
|
||||||
|
authenticateApiToken("api"),
|
||||||
|
requireApiScope("host", "get"),
|
||||||
|
async (req, res) => {
|
||||||
|
try {
|
||||||
|
const { hostgroup } = req.query;
|
||||||
|
|
||||||
|
let whereClause = {};
|
||||||
|
let filterValues = [];
|
||||||
|
|
||||||
|
// Parse hostgroup filter (comma-separated names or UUIDs)
|
||||||
|
if (hostgroup) {
|
||||||
|
filterValues = hostgroup.split(",").map((g) => g.trim());
|
||||||
|
|
||||||
|
// Separate UUIDs from names
|
||||||
|
const uuidFilters = [];
|
||||||
|
const nameFilters = [];
|
||||||
|
|
||||||
|
for (const value of filterValues) {
|
||||||
|
if (isUUID(value)) {
|
||||||
|
uuidFilters.push(value);
|
||||||
|
} else {
|
||||||
|
nameFilters.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find host group IDs from names
|
||||||
|
const groupIds = [...uuidFilters];
|
||||||
|
|
||||||
|
if (nameFilters.length > 0) {
|
||||||
|
const groups = await prisma.host_groups.findMany({
|
||||||
|
where: {
|
||||||
|
name: {
|
||||||
|
in: nameFilters,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add found group IDs
|
||||||
|
groupIds.push(...groups.map((g) => g.id));
|
||||||
|
|
||||||
|
// Check if any name filters didn't match
|
||||||
|
const foundNames = groups.map((g) => g.name);
|
||||||
|
const notFoundNames = nameFilters.filter(
|
||||||
|
(name) => !foundNames.includes(name),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (notFoundNames.length > 0) {
|
||||||
|
console.warn(`Host groups not found: ${notFoundNames.join(", ")}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter hosts by group memberships
|
||||||
|
if (groupIds.length > 0) {
|
||||||
|
whereClause = {
|
||||||
|
host_group_memberships: {
|
||||||
|
some: {
|
||||||
|
host_group_id: {
|
||||||
|
in: groupIds,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// No valid groups found, return empty result
|
||||||
|
return res.json({
|
||||||
|
hosts: [],
|
||||||
|
total: 0,
|
||||||
|
filtered_by_groups: filterValues,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query hosts with groups
|
||||||
|
const hosts = await prisma.hosts.findMany({
|
||||||
|
where: whereClause,
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
friendly_name: true,
|
||||||
|
hostname: true,
|
||||||
|
ip: true,
|
||||||
|
host_group_memberships: {
|
||||||
|
include: {
|
||||||
|
host_groups: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
name: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
friendly_name: "asc",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Format response
|
||||||
|
const formattedHosts = hosts.map((host) => ({
|
||||||
|
id: host.id,
|
||||||
|
friendly_name: host.friendly_name,
|
||||||
|
hostname: host.hostname,
|
||||||
|
ip: host.ip,
|
||||||
|
host_groups: host.host_group_memberships.map((membership) => ({
|
||||||
|
id: membership.host_groups.id,
|
||||||
|
name: membership.host_groups.name,
|
||||||
|
})),
|
||||||
|
}));
|
||||||
|
|
||||||
|
res.json({
|
||||||
|
hosts: formattedHosts,
|
||||||
|
total: formattedHosts.length,
|
||||||
|
filtered_by_groups: filterValues.length > 0 ? filterValues : undefined,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Error fetching hosts:", error);
|
||||||
|
res.status(500).json({ error: "Failed to fetch hosts" });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
module.exports = router;
|
||||||
@@ -125,6 +125,10 @@ router.post(
|
|||||||
.optional({ nullable: true, checkFalsy: true })
|
.optional({ nullable: true, checkFalsy: true })
|
||||||
.isISO8601()
|
.isISO8601()
|
||||||
.withMessage("Invalid date format"),
|
.withMessage("Invalid date format"),
|
||||||
|
body("scopes")
|
||||||
|
.optional()
|
||||||
|
.isObject()
|
||||||
|
.withMessage("Scopes must be an object"),
|
||||||
],
|
],
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -140,6 +144,7 @@ router.post(
|
|||||||
default_host_group_id,
|
default_host_group_id,
|
||||||
expires_at,
|
expires_at,
|
||||||
metadata = {},
|
metadata = {},
|
||||||
|
scopes,
|
||||||
} = req.body;
|
} = req.body;
|
||||||
|
|
||||||
// Validate host group if provided
|
// Validate host group if provided
|
||||||
@@ -153,6 +158,32 @@ router.post(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate scopes for API tokens
|
||||||
|
if (metadata.integration_type === "api" && scopes) {
|
||||||
|
// Validate scopes structure
|
||||||
|
if (typeof scopes !== "object" || scopes === null) {
|
||||||
|
return res.status(400).json({ error: "Scopes must be an object" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each resource in scopes
|
||||||
|
for (const [resource, actions] of Object.entries(scopes)) {
|
||||||
|
if (!Array.isArray(actions)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Scopes for resource "${resource}" must be an array of actions`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate action names
|
||||||
|
for (const action of actions) {
|
||||||
|
if (typeof action !== "string") {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `All actions in scopes must be strings`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const { token_key, token_secret } = generate_auto_enrollment_token();
|
const { token_key, token_secret } = generate_auto_enrollment_token();
|
||||||
const hashed_secret = await bcrypt.hash(token_secret, 10);
|
const hashed_secret = await bcrypt.hash(token_secret, 10);
|
||||||
|
|
||||||
@@ -168,6 +199,7 @@ router.post(
|
|||||||
default_host_group_id: default_host_group_id || null,
|
default_host_group_id: default_host_group_id || null,
|
||||||
expires_at: expires_at ? new Date(expires_at) : null,
|
expires_at: expires_at ? new Date(expires_at) : null,
|
||||||
metadata: { integration_type: "proxmox-lxc", ...metadata },
|
metadata: { integration_type: "proxmox-lxc", ...metadata },
|
||||||
|
scopes: metadata.integration_type === "api" ? scopes || null : null,
|
||||||
updated_at: new Date(),
|
updated_at: new Date(),
|
||||||
},
|
},
|
||||||
include: {
|
include: {
|
||||||
@@ -201,6 +233,7 @@ router.post(
|
|||||||
default_host_group: token.host_groups,
|
default_host_group: token.host_groups,
|
||||||
created_by: token.users,
|
created_by: token.users,
|
||||||
expires_at: token.expires_at,
|
expires_at: token.expires_at,
|
||||||
|
scopes: token.scopes,
|
||||||
},
|
},
|
||||||
warning: "⚠️ Save the token_secret now - it cannot be retrieved later!",
|
warning: "⚠️ Save the token_secret now - it cannot be retrieved later!",
|
||||||
});
|
});
|
||||||
@@ -232,6 +265,7 @@ router.get(
|
|||||||
created_at: true,
|
created_at: true,
|
||||||
default_host_group_id: true,
|
default_host_group_id: true,
|
||||||
metadata: true,
|
metadata: true,
|
||||||
|
scopes: true,
|
||||||
host_groups: {
|
host_groups: {
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -314,6 +348,10 @@ router.patch(
|
|||||||
body("max_hosts_per_day").optional().isInt({ min: 1, max: 1000 }),
|
body("max_hosts_per_day").optional().isInt({ min: 1, max: 1000 }),
|
||||||
body("allowed_ip_ranges").optional().isArray(),
|
body("allowed_ip_ranges").optional().isArray(),
|
||||||
body("expires_at").optional().isISO8601(),
|
body("expires_at").optional().isISO8601(),
|
||||||
|
body("scopes")
|
||||||
|
.optional()
|
||||||
|
.isObject()
|
||||||
|
.withMessage("Scopes must be an object"),
|
||||||
],
|
],
|
||||||
async (req, res) => {
|
async (req, res) => {
|
||||||
try {
|
try {
|
||||||
@@ -323,6 +361,16 @@ router.patch(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const { tokenId } = req.params;
|
const { tokenId } = req.params;
|
||||||
|
|
||||||
|
// First, get the existing token to check its integration type
|
||||||
|
const existing_token = await prisma.auto_enrollment_tokens.findUnique({
|
||||||
|
where: { id: tokenId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!existing_token) {
|
||||||
|
return res.status(404).json({ error: "Token not found" });
|
||||||
|
}
|
||||||
|
|
||||||
const update_data = { updated_at: new Date() };
|
const update_data = { updated_at: new Date() };
|
||||||
|
|
||||||
if (req.body.is_active !== undefined)
|
if (req.body.is_active !== undefined)
|
||||||
@@ -334,6 +382,41 @@ router.patch(
|
|||||||
if (req.body.expires_at !== undefined)
|
if (req.body.expires_at !== undefined)
|
||||||
update_data.expires_at = new Date(req.body.expires_at);
|
update_data.expires_at = new Date(req.body.expires_at);
|
||||||
|
|
||||||
|
// Handle scopes updates for API tokens only
|
||||||
|
if (req.body.scopes !== undefined) {
|
||||||
|
if (existing_token.metadata?.integration_type === "api") {
|
||||||
|
// Validate scopes structure
|
||||||
|
const scopes = req.body.scopes;
|
||||||
|
if (typeof scopes !== "object" || scopes === null) {
|
||||||
|
return res.status(400).json({ error: "Scopes must be an object" });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate each resource in scopes
|
||||||
|
for (const [resource, actions] of Object.entries(scopes)) {
|
||||||
|
if (!Array.isArray(actions)) {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `Scopes for resource "${resource}" must be an array of actions`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate action names
|
||||||
|
for (const action of actions) {
|
||||||
|
if (typeof action !== "string") {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: `All actions in scopes must be strings`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
update_data.scopes = scopes;
|
||||||
|
} else {
|
||||||
|
return res.status(400).json({
|
||||||
|
error: "Scopes can only be updated for API integration tokens",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const token = await prisma.auto_enrollment_tokens.update({
|
const token = await prisma.auto_enrollment_tokens.update({
|
||||||
where: { id: tokenId },
|
where: { id: tokenId },
|
||||||
data: update_data,
|
data: update_data,
|
||||||
|
|||||||
@@ -1,113 +1,12 @@
|
|||||||
const express = require("express");
|
const express = require("express");
|
||||||
const { getPrismaClient } = require("../config/prisma");
|
const { getPrismaClient } = require("../config/prisma");
|
||||||
const bcrypt = require("bcryptjs");
|
const { authenticateApiToken } = require("../middleware/apiAuth");
|
||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const prisma = getPrismaClient();
|
const prisma = getPrismaClient();
|
||||||
|
|
||||||
// Middleware to authenticate API key
|
|
||||||
const authenticateApiKey = async (req, res, next) => {
|
|
||||||
try {
|
|
||||||
const authHeader = req.headers.authorization;
|
|
||||||
|
|
||||||
if (!authHeader || !authHeader.startsWith("Basic ")) {
|
|
||||||
return res
|
|
||||||
.status(401)
|
|
||||||
.json({ error: "Missing or invalid authorization header" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 credentials
|
|
||||||
const base64Credentials = authHeader.split(" ")[1];
|
|
||||||
const credentials = Buffer.from(base64Credentials, "base64").toString(
|
|
||||||
"ascii",
|
|
||||||
);
|
|
||||||
const [apiKey, apiSecret] = credentials.split(":");
|
|
||||||
|
|
||||||
if (!apiKey || !apiSecret) {
|
|
||||||
return res.status(401).json({ error: "Invalid credentials format" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find the token in database
|
|
||||||
const token = await prisma.auto_enrollment_tokens.findUnique({
|
|
||||||
where: { token_key: apiKey },
|
|
||||||
include: {
|
|
||||||
users: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
username: true,
|
|
||||||
role: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
console.log(`API key not found: ${apiKey}`);
|
|
||||||
return res.status(401).json({ error: "Invalid API key" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token is active
|
|
||||||
if (!token.is_active) {
|
|
||||||
return res.status(401).json({ error: "API key is disabled" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token has expired
|
|
||||||
if (token.expires_at && new Date(token.expires_at) < new Date()) {
|
|
||||||
return res.status(401).json({ error: "API key has expired" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if token is for gethomepage integration
|
|
||||||
if (token.metadata?.integration_type !== "gethomepage") {
|
|
||||||
return res.status(401).json({ error: "Invalid API key type" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify the secret
|
|
||||||
const isValidSecret = await bcrypt.compare(apiSecret, token.token_secret);
|
|
||||||
if (!isValidSecret) {
|
|
||||||
return res.status(401).json({ error: "Invalid API secret" });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check IP restrictions if any
|
|
||||||
if (token.allowed_ip_ranges && token.allowed_ip_ranges.length > 0) {
|
|
||||||
const clientIp = req.ip || req.connection.remoteAddress;
|
|
||||||
const forwardedFor = req.headers["x-forwarded-for"];
|
|
||||||
const realIp = req.headers["x-real-ip"];
|
|
||||||
|
|
||||||
// Get the actual client IP (considering proxies)
|
|
||||||
const actualClientIp = forwardedFor
|
|
||||||
? forwardedFor.split(",")[0].trim()
|
|
||||||
: realIp || clientIp;
|
|
||||||
|
|
||||||
const isAllowedIp = token.allowed_ip_ranges.some((range) => {
|
|
||||||
// Simple IP range check (can be enhanced for CIDR support)
|
|
||||||
return actualClientIp.startsWith(range) || actualClientIp === range;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!isAllowedIp) {
|
|
||||||
console.log(
|
|
||||||
`IP validation failed. Client IP: ${actualClientIp}, Allowed ranges: ${token.allowed_ip_ranges.join(", ")}`,
|
|
||||||
);
|
|
||||||
return res.status(403).json({ error: "IP address not allowed" });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update last used timestamp
|
|
||||||
await prisma.auto_enrollment_tokens.update({
|
|
||||||
where: { id: token.id },
|
|
||||||
data: { last_used_at: new Date() },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Attach token info to request
|
|
||||||
req.apiToken = token;
|
|
||||||
next();
|
|
||||||
} catch (error) {
|
|
||||||
console.error("API key authentication error:", error);
|
|
||||||
res.status(500).json({ error: "Authentication failed" });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Get homepage widget statistics
|
// Get homepage widget statistics
|
||||||
router.get("/stats", authenticateApiKey, async (_req, res) => {
|
router.get("/stats", authenticateApiToken("gethomepage"), async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
// Get total hosts count
|
// Get total hosts count
|
||||||
const totalHosts = await prisma.hosts.count({
|
const totalHosts = await prisma.hosts.count({
|
||||||
@@ -235,7 +134,7 @@ router.get("/stats", authenticateApiKey, async (_req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Health check endpoint for the API
|
// Health check endpoint for the API
|
||||||
router.get("/health", authenticateApiKey, async (req, res) => {
|
router.get("/health", authenticateApiToken("gethomepage"), async (req, res) => {
|
||||||
res.json({
|
res.json({
|
||||||
status: "ok",
|
status: "ok",
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ const wsRoutes = require("./routes/wsRoutes");
|
|||||||
const agentVersionRoutes = require("./routes/agentVersionRoutes");
|
const agentVersionRoutes = require("./routes/agentVersionRoutes");
|
||||||
const metricsRoutes = require("./routes/metricsRoutes");
|
const metricsRoutes = require("./routes/metricsRoutes");
|
||||||
const userPreferencesRoutes = require("./routes/userPreferencesRoutes");
|
const userPreferencesRoutes = require("./routes/userPreferencesRoutes");
|
||||||
|
const apiHostsRoutes = require("./routes/apiHostsRoutes");
|
||||||
const { initSettings } = require("./services/settingsService");
|
const { initSettings } = require("./services/settingsService");
|
||||||
const { queueManager } = require("./services/automation");
|
const { queueManager } = require("./services/automation");
|
||||||
const { authenticateToken, requireAdmin } = require("./middleware/auth");
|
const { authenticateToken, requireAdmin } = require("./middleware/auth");
|
||||||
@@ -480,6 +481,7 @@ app.use(`/api/${apiVersion}/ws`, wsRoutes);
|
|||||||
app.use(`/api/${apiVersion}/agent`, agentVersionRoutes);
|
app.use(`/api/${apiVersion}/agent`, agentVersionRoutes);
|
||||||
app.use(`/api/${apiVersion}/metrics`, metricsRoutes);
|
app.use(`/api/${apiVersion}/metrics`, metricsRoutes);
|
||||||
app.use(`/api/${apiVersion}/user/preferences`, userPreferencesRoutes);
|
app.use(`/api/${apiVersion}/user/preferences`, userPreferencesRoutes);
|
||||||
|
app.use(`/api/${apiVersion}/api`, authLimiter, apiHostsRoutes);
|
||||||
|
|
||||||
// Bull Board - will be populated after queue manager initializes
|
// Bull Board - will be populated after queue manager initializes
|
||||||
let bullBoardRouter = null;
|
let bullBoardRouter = null;
|
||||||
|
|||||||
Reference in New Issue
Block a user