mirror of
https://github.com/fosrl/pangolin.git
synced 2026-05-27 03:02:30 +00:00
Compare commits
5 Commits
dev
...
auto-updat
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3539b9ddb4 | ||
|
|
4530aac4f3 | ||
|
|
6d4afd0953 | ||
|
|
dee0ca6864 | ||
|
|
ed73d089d0 |
5
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
5
.github/ISSUE_TEMPLATE/1.bug_report.yml
vendored
@@ -14,13 +14,12 @@ body:
|
|||||||
label: Environment
|
label: Environment
|
||||||
description: Please fill out the relevant details below for your environment.
|
description: Please fill out the relevant details below for your environment.
|
||||||
value: |
|
value: |
|
||||||
- OS Type & Version:
|
- OS Type & Version: (e.g., Ubuntu 22.04)
|
||||||
- Pangolin Version:
|
- Pangolin Version:
|
||||||
- Edition (Community or Enterprise):
|
|
||||||
- Gerbil Version:
|
- Gerbil Version:
|
||||||
- Traefik Version:
|
- Traefik Version:
|
||||||
- Newt Version:
|
- Newt Version:
|
||||||
- Client Version:
|
- Olm Version: (if applicable)
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
@@ -1601,7 +1601,17 @@
|
|||||||
"contents": "Contents",
|
"contents": "Contents",
|
||||||
"parsedContents": "Parsed Contents (Read Only)",
|
"parsedContents": "Parsed Contents (Read Only)",
|
||||||
"enableDockerSocket": "Enable Docker Blueprint",
|
"enableDockerSocket": "Enable Docker Blueprint",
|
||||||
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to Newt. Read about how this works in <docsLink>the documentation</docsLink>.",
|
"enableDockerSocketDescription": "Enable Docker Socket label scraping for blueprint labels. Socket path must be provided to the site connector. Read about how this works in <docsLink>the documentation</docsLink>.",
|
||||||
|
"newtAutoUpdate": "Enable Site Auto-Update",
|
||||||
|
"newtAutoUpdateDescription": "When enabled, site connectors will automatically update to the latest version when a new release is available.",
|
||||||
|
"siteAutoUpdate": "Site Auto-Update",
|
||||||
|
"siteAutoUpdateLabel": "Enable Auto-Update",
|
||||||
|
"siteAutoUpdateDescription": "Control whether this site's connector automatically downloads the latest version.",
|
||||||
|
"siteAutoUpdateOrgDefault": "Organization default: {state}",
|
||||||
|
"siteAutoUpdateOverriding": "Overriding organization setting",
|
||||||
|
"siteAutoUpdateResetToOrg": "Reset to Organization Default",
|
||||||
|
"siteAutoUpdateEnabled": "enabled",
|
||||||
|
"siteAutoUpdateDisabled": "disabled",
|
||||||
"viewDockerContainers": "View Docker Containers",
|
"viewDockerContainers": "View Docker Containers",
|
||||||
"containersIn": "Containers in {siteName}",
|
"containersIn": "Containers in {siteName}",
|
||||||
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
|
"selectContainerDescription": "Select any container to use as a hostname for this target. Click a port to use a port.",
|
||||||
@@ -1646,7 +1656,6 @@
|
|||||||
"certificateStatus": "Certificate",
|
"certificateStatus": "Certificate",
|
||||||
"certificateStatusAutoRefreshHint": "Status refreshes automatically.",
|
"certificateStatusAutoRefreshHint": "Status refreshes automatically.",
|
||||||
"loading": "Loading",
|
"loading": "Loading",
|
||||||
"loadingEllipsis": "Loading...",
|
|
||||||
"loadingAnalytics": "Loading Analytics",
|
"loadingAnalytics": "Loading Analytics",
|
||||||
"restart": "Restart",
|
"restart": "Restart",
|
||||||
"domains": "Domains",
|
"domains": "Domains",
|
||||||
|
|||||||
@@ -65,7 +65,12 @@ export const orgs = pgTable("orgs", {
|
|||||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
isBillingOrg: boolean("isBillingOrg"),
|
isBillingOrg: boolean("isBillingOrg"),
|
||||||
billingOrgId: varchar("billingOrgId")
|
billingOrgId: varchar("billingOrgId"),
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: boolean(
|
||||||
|
"settingsEnableGlobalNewtAutoUpdate"
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const orgDomains = pgTable("orgDomains", {
|
export const orgDomains = pgTable("orgDomains", {
|
||||||
@@ -103,6 +108,10 @@ export const sites = pgTable("sites", {
|
|||||||
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
lastHolePunch: bigint("lastHolePunch", { mode: "number" }),
|
||||||
listenPort: integer("listenPort"),
|
listenPort: integer("listenPort"),
|
||||||
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
dockerSocketEnabled: boolean("dockerSocketEnabled").notNull().default(true),
|
||||||
|
autoUpdateEnabled: boolean("autoUpdateEnabled").notNull().default(false),
|
||||||
|
autoUpdateOverrideOrg: boolean("autoUpdateOverrideOrg")
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
status: varchar("status")
|
status: varchar("status")
|
||||||
.$type<"pending" | "approved">()
|
.$type<"pending" | "approved">()
|
||||||
.default("approved")
|
.default("approved")
|
||||||
|
|||||||
@@ -62,7 +62,13 @@ export const orgs = sqliteTable("orgs", {
|
|||||||
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
sshCaPrivateKey: text("sshCaPrivateKey"), // Encrypted SSH CA private key (PEM format)
|
||||||
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
sshCaPublicKey: text("sshCaPublicKey"), // SSH CA public key (OpenSSH format)
|
||||||
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
|
isBillingOrg: integer("isBillingOrg", { mode: "boolean" }),
|
||||||
billingOrgId: text("billingOrgId")
|
billingOrgId: text("billingOrgId"),
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: integer(
|
||||||
|
"settingsEnableGlobalNewtAutoUpdate",
|
||||||
|
{ mode: "boolean" }
|
||||||
|
)
|
||||||
|
.notNull()
|
||||||
|
.default(false)
|
||||||
});
|
});
|
||||||
|
|
||||||
export const userDomains = sqliteTable("userDomains", {
|
export const userDomains = sqliteTable("userDomains", {
|
||||||
@@ -116,6 +122,14 @@ export const sites = sqliteTable("sites", {
|
|||||||
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
dockerSocketEnabled: integer("dockerSocketEnabled", { mode: "boolean" })
|
||||||
.notNull()
|
.notNull()
|
||||||
.default(true),
|
.default(true),
|
||||||
|
autoUpdateEnabled: integer("autoUpdateEnabled", { mode: "boolean" })
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
|
autoUpdateOverrideOrg: integer("autoUpdateOverrideOrg", {
|
||||||
|
mode: "boolean"
|
||||||
|
})
|
||||||
|
.notNull()
|
||||||
|
.default(false),
|
||||||
status: text("status").$type<"pending" | "approved">().default("approved")
|
status: text("status").$type<"pending" | "approved">().default("approved")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,8 @@ export enum TierFeature {
|
|||||||
StandaloneHealthChecks = "standaloneHealthChecks",
|
StandaloneHealthChecks = "standaloneHealthChecks",
|
||||||
AlertingRules = "alertingRules",
|
AlertingRules = "alertingRules",
|
||||||
WildcardSubdomain = "wildcardSubdomain",
|
WildcardSubdomain = "wildcardSubdomain",
|
||||||
Labels = "labels"
|
Labels = "labels",
|
||||||
|
NewtAutoUpdate = "newtAutoUpdate"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
export const tierMatrix: Record<TierFeature, Tier[]> = {
|
||||||
@@ -68,5 +69,6 @@ export const tierMatrix: Record<TierFeature, Tier[]> = {
|
|||||||
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
[TierFeature.DomainNamespaces]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
[TierFeature.StandaloneHealthChecks]: ["tier3", "enterprise"],
|
||||||
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
[TierFeature.AlertingRules]: ["tier3", "enterprise"],
|
||||||
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"]
|
[TierFeature.WildcardSubdomain]: ["tier1", "tier2", "tier3", "enterprise"],
|
||||||
|
[TierFeature.NewtAutoUpdate]: ["tier1", "tier2", "tier3", "enterprise"]
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -154,19 +154,8 @@ class AdaptiveCache {
|
|||||||
keys(): string[] {
|
keys(): string[] {
|
||||||
return localCache.keys();
|
return localCache.keys();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get keys with a specific prefix
|
|
||||||
* @param prefix - Key prefix to match
|
|
||||||
* @returns Array of matching keys
|
|
||||||
*/
|
|
||||||
async keysWithPrefix(prefix: string): Promise<string[]> {
|
|
||||||
const allKeys = localCache.keys();
|
|
||||||
return allKeys.filter((key) => key.startsWith(prefix));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Export singleton instance
|
// Export singleton instance
|
||||||
export const cache = new AdaptiveCache();
|
export const cache = new AdaptiveCache();
|
||||||
export const regionalCache = cache; // Alias for compatability with the private version
|
|
||||||
export default cache;
|
export default cache;
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db, logsDb, statusHistory } from "@server/db";
|
import { db, logsDb, statusHistory } from "@server/db";
|
||||||
import { and, eq, gte, asc } from "drizzle-orm";
|
import { and, eq, gte, asc } from "drizzle-orm";
|
||||||
import { regionalCache as cache } from "#dynamic/lib/cache";
|
import { regionalCache as cache } from "@server/private/lib/cache";
|
||||||
|
|
||||||
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
const STATUS_HISTORY_CACHE_TTL = 60; // seconds
|
||||||
|
|
||||||
|
|||||||
@@ -522,13 +522,13 @@ const sendToClientLocal = async (
|
|||||||
|
|
||||||
const messageString = JSON.stringify(messageWithVersion);
|
const messageString = JSON.stringify(messageWithVersion);
|
||||||
if (options.compress) {
|
if (options.compress) {
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Message size before compression: ${messageString.length} bytes`
|
// `Message size before compression: ${messageString.length} bytes`
|
||||||
);
|
// );
|
||||||
const compressed = zlib.gzipSync(Buffer.from(messageString, "utf8"));
|
const compressed = zlib.gzipSync(Buffer.from(messageString, "utf8"));
|
||||||
logger.debug(
|
// logger.debug(
|
||||||
`Message size after compression: ${compressed.length} bytes`
|
// `Message size after compression: ${compressed.length} bytes`
|
||||||
);
|
// );
|
||||||
clients.forEach((client) => {
|
clients.forEach((client) => {
|
||||||
if (client.readyState === WebSocket.OPEN) {
|
if (client.readyState === WebSocket.OPEN) {
|
||||||
client.send(compressed);
|
client.send(compressed);
|
||||||
|
|||||||
@@ -1231,6 +1231,22 @@ authRouter.post(
|
|||||||
newt.getNewtToken
|
newt.getNewtToken
|
||||||
);
|
);
|
||||||
|
|
||||||
|
authRouter.post(
|
||||||
|
"/newt/version",
|
||||||
|
rateLimit({
|
||||||
|
windowMs: 15 * 60 * 1000,
|
||||||
|
max: 60,
|
||||||
|
keyGenerator: (req) =>
|
||||||
|
`newtVersion:${req.body.newtId || ipKeyGenerator(req.ip || "")}`,
|
||||||
|
handler: (req, res, next) => {
|
||||||
|
const message = `You can only check the Newt version ${60} times every ${15} minutes. Please try again later.`;
|
||||||
|
return next(createHttpError(HttpCode.TOO_MANY_REQUESTS, message));
|
||||||
|
},
|
||||||
|
store: createStore()
|
||||||
|
}),
|
||||||
|
newt.getNewtVersion
|
||||||
|
);
|
||||||
|
|
||||||
authRouter.post(
|
authRouter.post(
|
||||||
"/newt/register",
|
"/newt/register",
|
||||||
rateLimit({
|
rateLimit({
|
||||||
|
|||||||
317
server/routers/newt/getNewtVersion.ts
Normal file
317
server/routers/newt/getNewtVersion.ts
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
import { db, orgs, sites } from "@server/db";
|
||||||
|
import { newts } from "@server/db";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { NextFunction, Request, Response } from "express";
|
||||||
|
import createHttpError from "http-errors";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { fromError } from "zod-validation-error";
|
||||||
|
import semver from "semver";
|
||||||
|
import { verifyPassword } from "@server/auth/password";
|
||||||
|
import response from "@server/lib/response";
|
||||||
|
import HttpCode from "@server/types/HttpCode";
|
||||||
|
import logger from "@server/logger";
|
||||||
|
import cache from "#dynamic/lib/cache";
|
||||||
|
import config from "@server/lib/config";
|
||||||
|
|
||||||
|
// Stale-while-revalidate in-memory fallback for the releases API.
|
||||||
|
type ReleaseInfo = {
|
||||||
|
version: string;
|
||||||
|
// binary filename -> sha256 hex (sourced from asset `digest` field in GitHub API)
|
||||||
|
assetDigests: Record<string, string>;
|
||||||
|
};
|
||||||
|
let staleReleaseInfo: ReleaseInfo | null = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the latest stable newt release from GitHub and returns the version
|
||||||
|
* tag together with a map of asset-name → sha256 hex digest.
|
||||||
|
* Results are cached for one hour; stale data is returned on failure.
|
||||||
|
*/
|
||||||
|
async function getLatestReleaseInfo(): Promise<ReleaseInfo | null> {
|
||||||
|
try {
|
||||||
|
const cached = await cache.get<ReleaseInfo>("cache:newtReleaseInfo");
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const controller = new AbortController();
|
||||||
|
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
||||||
|
|
||||||
|
const fetchResponse = await fetch(
|
||||||
|
"https://api.github.com/repos/fosrl/newt/releases",
|
||||||
|
{ signal: controller.signal }
|
||||||
|
);
|
||||||
|
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
|
||||||
|
if (!fetchResponse.ok) {
|
||||||
|
logger.warn(
|
||||||
|
`Failed to fetch Newt releases from GitHub: ${fetchResponse.status} ${fetchResponse.statusText}`
|
||||||
|
);
|
||||||
|
return staleReleaseInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
let releases: any[] = await fetchResponse.json();
|
||||||
|
if (!Array.isArray(releases) || releases.length === 0) {
|
||||||
|
logger.warn("No releases found for Newt repository");
|
||||||
|
return staleReleaseInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop drafts, pre-releases, and anything with "rc" in the tag name.
|
||||||
|
releases = releases.filter(
|
||||||
|
(r: any) =>
|
||||||
|
!r.draft &&
|
||||||
|
!r.prerelease &&
|
||||||
|
!r.tag_name.includes("rc") &&
|
||||||
|
!r.tag_name.includes("v")
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sort descending by semver to find the true latest stable release.
|
||||||
|
releases.sort((a: any, b: any) => {
|
||||||
|
const va = semver.coerce(a.tag_name);
|
||||||
|
const vb = semver.coerce(b.tag_name);
|
||||||
|
if (!va && !vb) return 0;
|
||||||
|
if (!va) return 1;
|
||||||
|
if (!vb) return -1;
|
||||||
|
return semver.rcompare(va, vb);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (releases.length === 0) {
|
||||||
|
logger.warn("No stable releases found for Newt repository");
|
||||||
|
return staleReleaseInfo;
|
||||||
|
}
|
||||||
|
|
||||||
|
const latest = releases[0];
|
||||||
|
const version: string = latest.tag_name;
|
||||||
|
|
||||||
|
// Build a map of binary filename → sha256 hex from the asset `digest`
|
||||||
|
// field returned by the GitHub API (format: "sha256:<hex>").
|
||||||
|
const assetDigests: Record<string, string> = {};
|
||||||
|
if (Array.isArray(latest.assets)) {
|
||||||
|
for (const asset of latest.assets) {
|
||||||
|
if (
|
||||||
|
typeof asset.name === "string" &&
|
||||||
|
typeof asset.digest === "string" &&
|
||||||
|
asset.digest.startsWith("sha256:")
|
||||||
|
) {
|
||||||
|
assetDigests[asset.name] = asset.digest.slice(
|
||||||
|
"sha256:".length
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const info: ReleaseInfo = { version, assetDigests };
|
||||||
|
staleReleaseInfo = info;
|
||||||
|
await cache.set("cache:newtReleaseInfo", info, 3600);
|
||||||
|
return info;
|
||||||
|
} catch (error: any) {
|
||||||
|
if (error.name === "AbortError") {
|
||||||
|
logger.warn("Request to fetch Newt releases timed out (5s)");
|
||||||
|
} else {
|
||||||
|
logger.warn(
|
||||||
|
"Error fetching Newt releases:",
|
||||||
|
error.message || error
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return staleReleaseInfo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const bodySchema = z.object({
|
||||||
|
newtId: z.string(),
|
||||||
|
secret: z.string(),
|
||||||
|
platform: z.string() // e.g. "linux_amd64", "darwin_arm64"
|
||||||
|
});
|
||||||
|
|
||||||
|
export type GetNewtVersionBody = z.infer<typeof bodySchema>;
|
||||||
|
|
||||||
|
export type GetNewtVersionResponse = {
|
||||||
|
latestVersion: string;
|
||||||
|
currentIsLatest: boolean;
|
||||||
|
downloadUrl: string;
|
||||||
|
sha256: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getNewtVersion(
|
||||||
|
req: Request,
|
||||||
|
res: Response,
|
||||||
|
next: NextFunction
|
||||||
|
): Promise<any> {
|
||||||
|
const parsedBody = bodySchema.safeParse(req.body);
|
||||||
|
|
||||||
|
if (!parsedBody.success) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.BAD_REQUEST,
|
||||||
|
fromError(parsedBody.error).toString()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { newtId, secret, platform } = parsedBody.data;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Verify newt credentials
|
||||||
|
const [existingNewt] = await db
|
||||||
|
.select()
|
||||||
|
.from(newts)
|
||||||
|
.where(eq(newts.newtId, newtId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingNewt) {
|
||||||
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
|
logger.info(
|
||||||
|
`Newt version check: no newt found with ID ${newtId}. IP: ${req.ip}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Invalid credentials")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existingNewt.siteId) {
|
||||||
|
logger.warn(`Newt ${newtId} has no associated site`);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.UNAUTHORIZED,
|
||||||
|
"Not associated with a site"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const validSecret = await verifyPassword(
|
||||||
|
secret,
|
||||||
|
existingNewt.secretHash
|
||||||
|
);
|
||||||
|
if (!validSecret) {
|
||||||
|
if (config.getRawConfig().app.log_failed_attempts) {
|
||||||
|
logger.info(
|
||||||
|
`Newt version check: invalid secret for newt ID ${newtId}. IP: ${req.ip}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return next(
|
||||||
|
createHttpError(HttpCode.UNAUTHORIZED, "Invalid credentials")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if udpates are enabled for the org or the site
|
||||||
|
const [site] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, existingNewt.siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!site) {
|
||||||
|
logger.warn(`Site with ID ${existingNewt.siteId} not found`);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Associated site not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [org] = await db
|
||||||
|
.select()
|
||||||
|
.from(orgs)
|
||||||
|
.where(eq(orgs.orgId, site.orgId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!org) {
|
||||||
|
logger.warn(`Org with ID ${site.orgId} not found`);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Associated organization not found"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let doUpdate = false;
|
||||||
|
|
||||||
|
if (site.autoUpdateOverrideOrg) {
|
||||||
|
doUpdate = site.autoUpdateEnabled;
|
||||||
|
} else {
|
||||||
|
doUpdate = org.settingsEnableGlobalNewtAutoUpdate;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!doUpdate) {
|
||||||
|
// return no content http code
|
||||||
|
return response(res, {
|
||||||
|
data: {
|
||||||
|
latestVersion: existingNewt.version ?? "",
|
||||||
|
currentIsLatest: true,
|
||||||
|
downloadUrl: "",
|
||||||
|
sha256: ""
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message:
|
||||||
|
"Auto-updates are disabled for this site and organization",
|
||||||
|
status: HttpCode.NO_CONTENT
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fetch latest release info (version + asset digests) in one API call.
|
||||||
|
const releaseInfo = await getLatestReleaseInfo();
|
||||||
|
|
||||||
|
if (!releaseInfo) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Unable to determine latest Newt version"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const latestVersion = releaseInfo.version;
|
||||||
|
|
||||||
|
// Binary name follows the get-newt.sh convention: newt_<platform>[.exe]
|
||||||
|
const binaryName = platform.includes("windows")
|
||||||
|
? `newt_${platform}.exe`
|
||||||
|
: `newt_${platform}`;
|
||||||
|
|
||||||
|
const downloadUrl = `https://github.com/fosrl/newt/releases/download/${latestVersion}/${binaryName}`;
|
||||||
|
|
||||||
|
// Look up the SHA256 digest for this specific binary from the GitHub
|
||||||
|
// release asset metadata (the `digest` field, format "sha256:<hex>").
|
||||||
|
const sha256 = releaseInfo.assetDigests[binaryName] ?? "";
|
||||||
|
|
||||||
|
// Determine whether the newt that's asking is already up to date.
|
||||||
|
// We store the current version on the newt row when it registers.
|
||||||
|
const currentVersion = existingNewt.version ?? null;
|
||||||
|
let currentIsLatest = false;
|
||||||
|
if (currentVersion) {
|
||||||
|
try {
|
||||||
|
const latest = semver.coerce(latestVersion);
|
||||||
|
const current = semver.coerce(currentVersion);
|
||||||
|
if (latest && current) {
|
||||||
|
currentIsLatest = !semver.lt(current, latest);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// If we can't compare, assume not latest
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return response<GetNewtVersionResponse>(res, {
|
||||||
|
data: {
|
||||||
|
latestVersion,
|
||||||
|
currentIsLatest,
|
||||||
|
downloadUrl,
|
||||||
|
sha256
|
||||||
|
},
|
||||||
|
success: true,
|
||||||
|
error: false,
|
||||||
|
message: "Version info retrieved successfully",
|
||||||
|
status: HttpCode.OK
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.INTERNAL_SERVER_ERROR,
|
||||||
|
"Failed to retrieve version info"
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
export * from "./createNewt";
|
export * from "./createNewt";
|
||||||
export * from "./getNewtToken";
|
export * from "./getNewtToken";
|
||||||
|
export * from "./getNewtVersion";
|
||||||
export * from "./handleNewtRegisterMessage";
|
export * from "./handleNewtRegisterMessage";
|
||||||
export * from "./handleReceiveBandwidthMessage";
|
export * from "./handleReceiveBandwidthMessage";
|
||||||
export * from "./handleNewtGetConfigMessage";
|
export * from "./handleNewtGetConfigMessage";
|
||||||
|
|||||||
@@ -40,7 +40,8 @@ const updateOrgBodySchema = z
|
|||||||
settingsLogRetentionDaysConnection: z
|
settingsLogRetentionDaysConnection: z
|
||||||
.number()
|
.number()
|
||||||
.min(build === "saas" ? 0 : -1)
|
.min(build === "saas" ? 0 : -1)
|
||||||
.optional()
|
.optional(),
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: z.boolean().optional()
|
||||||
})
|
})
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
@@ -118,6 +119,15 @@ export async function updateOrg(
|
|||||||
if (!hasPasswordExpirationFeature) {
|
if (!hasPasswordExpirationFeature) {
|
||||||
parsedBody.data.passwordExpiryDays = undefined;
|
parsedBody.data.passwordExpiryDays = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasNewtAutoUpdateFeature = await isLicensedOrSubscribed(
|
||||||
|
orgId,
|
||||||
|
tierMatrix[TierFeature.NewtAutoUpdate]
|
||||||
|
);
|
||||||
|
if (!hasNewtAutoUpdateFeature) {
|
||||||
|
parsedBody.data.settingsEnableGlobalNewtAutoUpdate = false; // force it off
|
||||||
|
}
|
||||||
|
|
||||||
if (build == "saas") {
|
if (build == "saas") {
|
||||||
const { tier } = await getOrgTierData(orgId);
|
const { tier } = await getOrgTierData(orgId);
|
||||||
|
|
||||||
@@ -136,8 +146,10 @@ export async function updateOrg(
|
|||||||
|
|
||||||
if (maxRetentionDays !== null) {
|
if (maxRetentionDays !== null) {
|
||||||
if (
|
if (
|
||||||
parsedBody.data.settingsLogRetentionDaysRequest !== undefined &&
|
parsedBody.data.settingsLogRetentionDaysRequest !==
|
||||||
parsedBody.data.settingsLogRetentionDaysRequest > maxRetentionDays
|
undefined &&
|
||||||
|
parsedBody.data.settingsLogRetentionDaysRequest >
|
||||||
|
maxRetentionDays
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -147,8 +159,10 @@ export async function updateOrg(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
parsedBody.data.settingsLogRetentionDaysAccess !== undefined &&
|
parsedBody.data.settingsLogRetentionDaysAccess !==
|
||||||
parsedBody.data.settingsLogRetentionDaysAccess > maxRetentionDays
|
undefined &&
|
||||||
|
parsedBody.data.settingsLogRetentionDaysAccess >
|
||||||
|
maxRetentionDays
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -158,8 +172,10 @@ export async function updateOrg(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
parsedBody.data.settingsLogRetentionDaysAction !== undefined &&
|
parsedBody.data.settingsLogRetentionDaysAction !==
|
||||||
parsedBody.data.settingsLogRetentionDaysAction > maxRetentionDays
|
undefined &&
|
||||||
|
parsedBody.data.settingsLogRetentionDaysAction >
|
||||||
|
maxRetentionDays
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -169,8 +185,10 @@ export async function updateOrg(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
parsedBody.data.settingsLogRetentionDaysConnection !== undefined &&
|
parsedBody.data.settingsLogRetentionDaysConnection !==
|
||||||
parsedBody.data.settingsLogRetentionDaysConnection > maxRetentionDays
|
undefined &&
|
||||||
|
parsedBody.data.settingsLogRetentionDaysConnection >
|
||||||
|
maxRetentionDays
|
||||||
) {
|
) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
@@ -196,7 +214,9 @@ export async function updateOrg(
|
|||||||
settingsLogRetentionDaysAction:
|
settingsLogRetentionDaysAction:
|
||||||
parsedBody.data.settingsLogRetentionDaysAction,
|
parsedBody.data.settingsLogRetentionDaysAction,
|
||||||
settingsLogRetentionDaysConnection:
|
settingsLogRetentionDaysConnection:
|
||||||
parsedBody.data.settingsLogRetentionDaysConnection
|
parsedBody.data.settingsLogRetentionDaysConnection,
|
||||||
|
settingsEnableGlobalNewtAutoUpdate:
|
||||||
|
parsedBody.data.settingsEnableGlobalNewtAutoUpdate
|
||||||
})
|
})
|
||||||
.where(eq(orgs.orgId, orgId))
|
.where(eq(orgs.orgId, orgId))
|
||||||
.returning();
|
.returning();
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { Request, Response, NextFunction } from "express";
|
import { Request, Response, NextFunction } from "express";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { db } from "@server/db";
|
import { db, Site } from "@server/db";
|
||||||
import { sites } from "@server/db";
|
import { sites } from "@server/db";
|
||||||
import { eq, and, ne } from "drizzle-orm";
|
import { eq, and, ne } from "drizzle-orm";
|
||||||
import response from "@server/lib/response";
|
import response from "@server/lib/response";
|
||||||
@@ -9,7 +9,8 @@ import createHttpError from "http-errors";
|
|||||||
import logger from "@server/logger";
|
import logger from "@server/logger";
|
||||||
import { fromError } from "zod-validation-error";
|
import { fromError } from "zod-validation-error";
|
||||||
import { OpenAPITags, registry } from "@server/openApi";
|
import { OpenAPITags, registry } from "@server/openApi";
|
||||||
import { isValidCIDR } from "@server/lib/validators";
|
import { isLicensedOrSubscribed } from "#dynamic/lib/isLicencedOrSubscribed";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
|
|
||||||
const updateSiteParamsSchema = z.strictObject({
|
const updateSiteParamsSchema = z.strictObject({
|
||||||
siteId: z.string().transform(Number).pipe(z.int().positive())
|
siteId: z.string().transform(Number).pipe(z.int().positive())
|
||||||
@@ -21,18 +22,8 @@ const updateSiteBodySchema = z
|
|||||||
niceId: z.string().min(1).max(255).optional(),
|
niceId: z.string().min(1).max(255).optional(),
|
||||||
dockerSocketEnabled: z.boolean().optional(),
|
dockerSocketEnabled: z.boolean().optional(),
|
||||||
status: z.enum(["pending", "approved"]).optional(),
|
status: z.enum(["pending", "approved"]).optional(),
|
||||||
// remoteSubnets: z.string().optional()
|
autoUpdateEnabled: z.boolean().optional(),
|
||||||
// subdomain: z
|
autoUpdateOverrideOrg: z.boolean().optional()
|
||||||
// .string()
|
|
||||||
// .min(1)
|
|
||||||
// .max(255)
|
|
||||||
// .transform((val) => val.toLowerCase())
|
|
||||||
// .optional()
|
|
||||||
// pubKey: z.string().optional(),
|
|
||||||
// subnet: z.string().optional(),
|
|
||||||
// exitNode: z.number().int().positive().optional(),
|
|
||||||
// megabytesIn: z.number().int().nonnegative().optional(),
|
|
||||||
// megabytesOut: z.number().int().nonnegative().optional(),
|
|
||||||
})
|
})
|
||||||
.refine((data) => Object.keys(data).length > 0, {
|
.refine((data) => Object.keys(data).length > 0, {
|
||||||
error: "At least one field must be provided for update"
|
error: "At least one field must be provided for update"
|
||||||
@@ -85,9 +76,24 @@ export async function updateSite(
|
|||||||
const { siteId } = parsedParams.data;
|
const { siteId } = parsedParams.data;
|
||||||
const updateData = parsedBody.data;
|
const updateData = parsedBody.data;
|
||||||
|
|
||||||
|
const [existingSite] = await db
|
||||||
|
.select()
|
||||||
|
.from(sites)
|
||||||
|
.where(eq(sites.siteId, siteId))
|
||||||
|
.limit(1);
|
||||||
|
|
||||||
|
if (!existingSite) {
|
||||||
|
return next(
|
||||||
|
createHttpError(
|
||||||
|
HttpCode.NOT_FOUND,
|
||||||
|
`Site with ID ${siteId} not found`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// if niceId is provided, check if it's already in use by another site
|
// if niceId is provided, check if it's already in use by another site
|
||||||
if (updateData.niceId) {
|
if (updateData.niceId) {
|
||||||
const [existingSite] = await db
|
const [existingSiteNiceIdOverlap] = await db
|
||||||
.select()
|
.select()
|
||||||
.from(sites)
|
.from(sites)
|
||||||
.where(
|
.where(
|
||||||
@@ -99,7 +105,7 @@ export async function updateSite(
|
|||||||
)
|
)
|
||||||
.limit(1);
|
.limit(1);
|
||||||
|
|
||||||
if (existingSite) {
|
if (existingSiteNiceIdOverlap) {
|
||||||
return next(
|
return next(
|
||||||
createHttpError(
|
createHttpError(
|
||||||
HttpCode.CONFLICT,
|
HttpCode.CONFLICT,
|
||||||
@@ -109,6 +115,15 @@ export async function updateSite(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hasNewtAutoUpdateFeature = await isLicensedOrSubscribed(
|
||||||
|
existingSite.orgId,
|
||||||
|
tierMatrix[TierFeature.NewtAutoUpdate]
|
||||||
|
);
|
||||||
|
if (!hasNewtAutoUpdateFeature) {
|
||||||
|
parsedBody.data.autoUpdateEnabled = false; // force it off
|
||||||
|
parsedBody.data.autoUpdateOverrideOrg = false; // force it off
|
||||||
|
}
|
||||||
|
|
||||||
// // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
|
// // if remoteSubnets is provided, ensure it's a valid comma-separated list of cidrs
|
||||||
// if (updateData.remoteSubnets) {
|
// if (updateData.remoteSubnets) {
|
||||||
// const subnets = updateData.remoteSubnets
|
// const subnets = updateData.remoteSubnets
|
||||||
|
|||||||
@@ -38,11 +38,16 @@ import { useUserContext } from "@app/hooks/useUserContext";
|
|||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import type { OrgContextType } from "@app/contexts/orgContext";
|
import type { OrgContextType } from "@app/contexts/orgContext";
|
||||||
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
|
|
||||||
// Schema for general organization settings
|
// Schema for general organization settings
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string(),
|
name: z.string(),
|
||||||
subnet: z.string().optional()
|
subnet: z.string().optional(),
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
@@ -163,17 +168,24 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
resolver: zodResolver(
|
resolver: zodResolver(
|
||||||
GeneralFormSchema.pick({
|
GeneralFormSchema.pick({
|
||||||
name: true,
|
name: true,
|
||||||
subnet: true
|
subnet: true,
|
||||||
|
settingsEnableGlobalNewtAutoUpdate: true
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: org.name,
|
name: org.name,
|
||||||
subnet: org.subnet || "" // Add default value for subnet
|
subnet: org.subnet || "",
|
||||||
|
settingsEnableGlobalNewtAutoUpdate:
|
||||||
|
org.settingsEnableGlobalNewtAutoUpdate ?? false
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const hasAutoUpdateFeature = isPaidUser(
|
||||||
|
tierMatrix[TierFeature.NewtAutoUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
const [, formAction, loadingSave] = useActionState(performSave, null);
|
const [, formAction, loadingSave] = useActionState(performSave, null);
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
@@ -186,7 +198,9 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const reqData = {
|
const reqData = {
|
||||||
name: data.name
|
name: data.name,
|
||||||
|
settingsEnableGlobalNewtAutoUpdate:
|
||||||
|
data.settingsEnableGlobalNewtAutoUpdate
|
||||||
} as any;
|
} as any;
|
||||||
|
|
||||||
// Update organization
|
// Update organization
|
||||||
@@ -194,13 +208,16 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
|
|
||||||
// Update the org context to reflect the change in the info card
|
// Update the org context to reflect the change in the info card
|
||||||
updateOrg({
|
updateOrg({
|
||||||
name: data.name
|
name: data.name,
|
||||||
|
settingsEnableGlobalNewtAutoUpdate:
|
||||||
|
data.settingsEnableGlobalNewtAutoUpdate
|
||||||
});
|
});
|
||||||
|
|
||||||
toast({
|
toast({
|
||||||
title: t("orgUpdated"),
|
title: t("orgUpdated"),
|
||||||
description: t("orgUpdatedDescription")
|
description: t("orgUpdatedDescription")
|
||||||
});
|
});
|
||||||
|
|
||||||
router.refresh();
|
router.refresh();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
toast({
|
toast({
|
||||||
@@ -243,6 +260,31 @@ function GeneralSectionForm({ org }: SectionFormProps) {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<PaidFeaturesAlert
|
||||||
|
tiers={tierMatrix.newtAutoUpdate}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="settingsEnableGlobalNewtAutoUpdate"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<SwitchInput
|
||||||
|
id="settings-enable-global-newt-auto-update"
|
||||||
|
label={t("newtAutoUpdate")}
|
||||||
|
checked={field.value}
|
||||||
|
onCheckedChange={field.onChange}
|
||||||
|
disabled={!hasAutoUpdateFeature}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t("newtAutoUpdateDescription")}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { Button } from "@app/components/ui/button";
|
import { Button } from "@app/components/ui/button";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { useState, useTransition, useMemo } from "react";
|
import { useState, useRef, useEffect, useTransition } from "react";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
@@ -20,9 +20,6 @@ import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
|||||||
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import { logQueries } from "@app/lib/queries";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import type { QueryAccessAuditLogResponse } from "@server/routers/auditLogs/types";
|
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -33,8 +30,23 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [isExporting, startTransition] = useTransition();
|
const [isExporting, startTransition] = useTransition();
|
||||||
|
const [filterAttributes, setFilterAttributes] = useState<{
|
||||||
|
actors: string[];
|
||||||
|
resources: {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
}[];
|
||||||
|
locations: string[];
|
||||||
|
}>({
|
||||||
|
actors: [],
|
||||||
|
resources: [],
|
||||||
|
locations: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter states - unified object for all filters
|
||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
action?: string;
|
action?: string;
|
||||||
type?: string;
|
type?: string;
|
||||||
@@ -49,21 +61,40 @@ export default function GeneralPage() {
|
|||||||
actor: searchParams.get("actor") || undefined
|
actor: searchParams.get("actor") || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Initialize page size from storage or default
|
||||||
const [pageSize, setPageSize] = useStoredPageSize("access-audit-logs", 20);
|
const [pageSize, setPageSize] = useStoredPageSize("access-audit-logs", 20);
|
||||||
|
|
||||||
|
// Set default date range to last 24 hours
|
||||||
const getDefaultDateRange = () => {
|
const getDefaultDateRange = () => {
|
||||||
|
// if the time is in the url params, use that instead
|
||||||
const startParam = searchParams.get("start");
|
const startParam = searchParams.get("start");
|
||||||
const endParam = searchParams.get("end");
|
const endParam = searchParams.get("end");
|
||||||
if (startParam && endParam) {
|
if (startParam && endParam) {
|
||||||
return {
|
return {
|
||||||
startDate: { date: new Date(startParam) },
|
startDate: {
|
||||||
endDate: { date: new Date(endParam) }
|
date: new Date(startParam)
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
date: new Date(endParam)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const lastWeek = getSevenDaysAgo();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startDate: { date: getSevenDaysAgo() },
|
startDate: {
|
||||||
endDate: { date: new Date() }
|
date: lastWeek
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
date: now
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,95 +103,75 @@ export default function GeneralPage() {
|
|||||||
endDate: DateTimeValue;
|
endDate: DateTimeValue;
|
||||||
}>(getDefaultDateRange());
|
}>(getDefaultDateRange());
|
||||||
|
|
||||||
const queryFilters = useMemo(() => {
|
// Trigger search with default values on component mount
|
||||||
let timeStart: string | undefined;
|
useEffect(() => {
|
||||||
let timeEnd: string | undefined;
|
const defaultRange = getDefaultDateRange();
|
||||||
|
queryDateTime(
|
||||||
if (dateRange.startDate?.date) {
|
defaultRange.startDate,
|
||||||
const dt = new Date(dateRange.startDate.date);
|
defaultRange.endDate,
|
||||||
if (dateRange.startDate.time) {
|
0,
|
||||||
const [h, m, s] = dateRange.startDate.time
|
pageSize
|
||||||
.split(":")
|
|
||||||
.map(Number);
|
|
||||||
dt.setHours(h, m, s || 0);
|
|
||||||
}
|
|
||||||
timeStart = dt.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dateRange.endDate?.date) {
|
|
||||||
const dt = new Date(dateRange.endDate.date);
|
|
||||||
if (dateRange.endDate.time) {
|
|
||||||
const [h, m, s] = dateRange.endDate.time.split(":").map(Number);
|
|
||||||
dt.setHours(h, m, s || 0);
|
|
||||||
} else {
|
|
||||||
const now = new Date();
|
|
||||||
dt.setHours(
|
|
||||||
now.getHours(),
|
|
||||||
now.getMinutes(),
|
|
||||||
now.getSeconds(),
|
|
||||||
now.getMilliseconds()
|
|
||||||
);
|
);
|
||||||
}
|
}, [orgId]); // Re-run if orgId changes
|
||||||
timeEnd = dt.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeStart,
|
|
||||||
timeEnd,
|
|
||||||
page: currentPage,
|
|
||||||
pageSize,
|
|
||||||
...filters,
|
|
||||||
resourceId: filters.resourceId
|
|
||||||
? Number(filters.resourceId)
|
|
||||||
: undefined
|
|
||||||
};
|
|
||||||
}, [dateRange, currentPage, pageSize, filters]);
|
|
||||||
|
|
||||||
const { data, isFetching, isLoading, refetch } = useQuery({
|
|
||||||
...logQueries.access({
|
|
||||||
orgId: orgId as string,
|
|
||||||
filters: queryFilters
|
|
||||||
}),
|
|
||||||
enabled: isPaidUser(tierMatrix.accessLogs) && build !== "oss"
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = isLoading ? generateSampleAccessLogs() : (data?.log ?? []);
|
|
||||||
const totalCount = data?.pagination?.total ?? 0;
|
|
||||||
const filterAttributes = data?.filterAttributes ?? {
|
|
||||||
actors: [],
|
|
||||||
resources: [],
|
|
||||||
locations: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDateRangeChange = (
|
const handleDateRangeChange = (
|
||||||
startDate: DateTimeValue,
|
startDate: DateTimeValue,
|
||||||
endDate: DateTimeValue
|
endDate: DateTimeValue
|
||||||
) => {
|
) => {
|
||||||
setDateRange({ startDate, endDate });
|
setDateRange({ startDate, endDate });
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when filtering
|
||||||
|
// put the search params in the url for the time
|
||||||
updateUrlParamsForAllFilters({
|
updateUrlParamsForAllFilters({
|
||||||
start: startDate.date?.toISOString() || "",
|
start: startDate.date?.toISOString() || "",
|
||||||
end: endDate.date?.toISOString() || ""
|
end: endDate.date?.toISOString() || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryDateTime(startDate, endDate, 0, pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page changes
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
setCurrentPage(newPage);
|
setCurrentPage(newPage);
|
||||||
|
queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
newPage,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page size changes
|
||||||
const handlePageSizeChange = (newPageSize: number) => {
|
const handlePageSizeChange = (newPageSize: number) => {
|
||||||
setPageSize(newPageSize);
|
setPageSize(newPageSize);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when changing page size
|
||||||
|
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle filter changes generically
|
||||||
const handleFilterChange = (
|
const handleFilterChange = (
|
||||||
filterType: keyof typeof filters,
|
filterType: keyof typeof filters,
|
||||||
value: string | undefined
|
value: string | undefined
|
||||||
) => {
|
) => {
|
||||||
const newFilters = { ...filters, [filterType]: value };
|
// Create new filters object with updated value
|
||||||
|
const newFilters = {
|
||||||
|
...filters,
|
||||||
|
[filterType]: value
|
||||||
|
};
|
||||||
|
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when filtering
|
||||||
|
|
||||||
|
// Update URL params
|
||||||
updateUrlParamsForAllFilters(newFilters);
|
updateUrlParamsForAllFilters(newFilters);
|
||||||
|
|
||||||
|
// Trigger new query with updated filters (pass directly to avoid async state issues)
|
||||||
|
queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
0,
|
||||||
|
pageSize,
|
||||||
|
newFilters
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUrlParamsForAllFilters = (
|
const updateUrlParamsForAllFilters = (
|
||||||
@@ -182,8 +193,114 @@ export default function GeneralPage() {
|
|||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
router.replace(`?${params.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryDateTime = async (
|
||||||
|
startDate: DateTimeValue,
|
||||||
|
endDate: DateTimeValue,
|
||||||
|
page: number = currentPage,
|
||||||
|
size: number = pageSize,
|
||||||
|
filtersParam?: {
|
||||||
|
action?: string;
|
||||||
|
type?: string;
|
||||||
|
resourceId?: string;
|
||||||
|
location?: string;
|
||||||
|
actor?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||||
|
if (!isPaidUser(tierMatrix.accessLogs) || build === "oss") {
|
||||||
|
console.log(
|
||||||
|
"Access denied: subscription inactive or license locked"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the provided filters or fall back to current state
|
||||||
|
const activeFilters = filtersParam || filters;
|
||||||
|
|
||||||
|
// Convert the date/time values to API parameters
|
||||||
|
const params: any = {
|
||||||
|
limit: size,
|
||||||
|
offset: page * size,
|
||||||
|
...activeFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startDate?.date) {
|
||||||
|
const startDateTime = new Date(startDate.date);
|
||||||
|
if (startDate.time) {
|
||||||
|
const [hours, minutes, seconds] = startDate.time
|
||||||
|
.split(":")
|
||||||
|
.map(Number);
|
||||||
|
startDateTime.setHours(hours, minutes, seconds || 0);
|
||||||
|
}
|
||||||
|
params.timeStart = startDateTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate?.date) {
|
||||||
|
const endDateTime = new Date(endDate.date);
|
||||||
|
if (endDate.time) {
|
||||||
|
const [hours, minutes, seconds] = endDate.time
|
||||||
|
.split(":")
|
||||||
|
.map(Number);
|
||||||
|
endDateTime.setHours(hours, minutes, seconds || 0);
|
||||||
|
} else {
|
||||||
|
// If no time is specified, set to NOW
|
||||||
|
const now = new Date();
|
||||||
|
endDateTime.setHours(
|
||||||
|
now.getHours(),
|
||||||
|
now.getMinutes(),
|
||||||
|
now.getSeconds(),
|
||||||
|
now.getMilliseconds()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
params.timeEnd = endDateTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.get(`/org/${orgId}/logs/access`, { params });
|
||||||
|
if (res.status === 200) {
|
||||||
|
setRows(res.data.data.log || []);
|
||||||
|
setTotalCount(res.data.data.pagination?.total || 0);
|
||||||
|
setFilterAttributes(res.data.data.filterAttributes);
|
||||||
|
console.log("Fetched logs:", res.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("Failed to filter logs"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
// Refresh data with current date range and pagination
|
||||||
|
await queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
currentPage,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const exportData = async () => {
|
const exportData = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Prepare query params for export
|
||||||
const params: any = {
|
const params: any = {
|
||||||
timeStart: dateRange.startDate?.date
|
timeStart: dateRange.startDate?.date
|
||||||
? new Date(dateRange.startDate.date).toISOString()
|
? new Date(dateRange.startDate.date).toISOString()
|
||||||
@@ -199,6 +316,7 @@ export default function GeneralPage() {
|
|||||||
params
|
params
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create a URL for the blob and trigger a download
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -216,6 +334,7 @@ export default function GeneralPage() {
|
|||||||
const data = error.response.data;
|
const data = error.response.data;
|
||||||
|
|
||||||
if (data instanceof Blob && data.type === "application/json") {
|
if (data instanceof Blob && data.type === "application/json") {
|
||||||
|
// Parse the Blob as JSON
|
||||||
const text = await data.text();
|
const text = await data.text();
|
||||||
const errorData = JSON.parse(text);
|
const errorData = JSON.parse(text);
|
||||||
apiErrorMessage = errorData.message;
|
apiErrorMessage = errorData.message;
|
||||||
@@ -232,7 +351,7 @@ export default function GeneralPage() {
|
|||||||
const columns: ColumnDef<any>[] = [
|
const columns: ColumnDef<any>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "timestamp",
|
accessorKey: "timestamp",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return t("timestamp");
|
return t("timestamp");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -247,7 +366,7 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "action",
|
accessorKey: "action",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("action")}</span>
|
<span>{t("action")}</span>
|
||||||
@@ -260,6 +379,7 @@ export default function GeneralPage() {
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleFilterChange("action", value)
|
handleFilterChange("action", value)
|
||||||
}
|
}
|
||||||
|
// placeholder=""
|
||||||
searchPlaceholder="Search..."
|
searchPlaceholder="Search..."
|
||||||
emptyMessage="None found"
|
emptyMessage="None found"
|
||||||
/>
|
/>
|
||||||
@@ -276,11 +396,13 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "ip",
|
accessorKey: "ip",
|
||||||
header: () => t("ip")
|
header: ({ column }) => {
|
||||||
|
return t("ip");
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "location",
|
accessorKey: "location",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("location")}</span>
|
<span>{t("location")}</span>
|
||||||
@@ -295,6 +417,7 @@ export default function GeneralPage() {
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleFilterChange("location", value)
|
handleFilterChange("location", value)
|
||||||
}
|
}
|
||||||
|
// placeholder=""
|
||||||
searchPlaceholder="Search..."
|
searchPlaceholder="Search..."
|
||||||
emptyMessage="None found"
|
emptyMessage="None found"
|
||||||
/>
|
/>
|
||||||
@@ -319,7 +442,7 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "resourceName",
|
accessorKey: "resourceName",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("resource")}</span>
|
<span>{t("resource")}</span>
|
||||||
@@ -332,6 +455,7 @@ export default function GeneralPage() {
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleFilterChange("resourceId", value)
|
handleFilterChange("resourceId", value)
|
||||||
}
|
}
|
||||||
|
// placeholder=""
|
||||||
searchPlaceholder="Search..."
|
searchPlaceholder="Search..."
|
||||||
emptyMessage="None found"
|
emptyMessage="None found"
|
||||||
/>
|
/>
|
||||||
@@ -357,7 +481,7 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "type",
|
accessorKey: "type",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("type")}</span>
|
<span>{t("type")}</span>
|
||||||
@@ -376,6 +500,7 @@ export default function GeneralPage() {
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleFilterChange("type", value)
|
handleFilterChange("type", value)
|
||||||
}
|
}
|
||||||
|
// placeholder=""
|
||||||
searchPlaceholder="Search..."
|
searchPlaceholder="Search..."
|
||||||
emptyMessage="None found"
|
emptyMessage="None found"
|
||||||
/>
|
/>
|
||||||
@@ -393,7 +518,7 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "actor",
|
accessorKey: "actor",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("actor")}</span>
|
<span>{t("actor")}</span>
|
||||||
@@ -406,6 +531,7 @@ export default function GeneralPage() {
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleFilterChange("actor", value)
|
handleFilterChange("actor", value)
|
||||||
}
|
}
|
||||||
|
// placeholder=""
|
||||||
searchPlaceholder="Search..."
|
searchPlaceholder="Search..."
|
||||||
emptyMessage="None found"
|
emptyMessage="None found"
|
||||||
/>
|
/>
|
||||||
@@ -433,12 +559,16 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "actorId",
|
accessorKey: "actorId",
|
||||||
header: () => t("actorId"),
|
header: ({ column }) => {
|
||||||
cell: ({ row }) => (
|
return t("actorId");
|
||||||
|
},
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
<span className="flex items-center gap-1">
|
<span className="flex items-center gap-1">
|
||||||
{row.original.actorId || "-"}
|
{row.original.actorId || "-"}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -484,10 +614,13 @@ export default function GeneralPage() {
|
|||||||
columns={columns}
|
columns={columns}
|
||||||
data={rows}
|
data={rows}
|
||||||
title={t("accessLogs")}
|
title={t("accessLogs")}
|
||||||
onRefresh={() => refetch()}
|
onRefresh={refreshData}
|
||||||
isRefreshing={isFetching}
|
isRefreshing={isRefreshing}
|
||||||
onExport={() => startTransition(exportData)}
|
onExport={() => startTransition(exportData)}
|
||||||
isExporting={isExporting}
|
isExporting={isExporting}
|
||||||
|
// isExportDisabled={ // not disabling this because the user should be able to click the button and get the feedback about needing to upgrade the plan
|
||||||
|
// !isPaidUser(tierMatrix.accessLogs) || build === "oss"
|
||||||
|
// }
|
||||||
onDateRangeChange={handleDateRangeChange}
|
onDateRangeChange={handleDateRangeChange}
|
||||||
dateRange={{
|
dateRange={{
|
||||||
start: dateRange.startDate,
|
start: dateRange.startDate,
|
||||||
@@ -497,12 +630,14 @@ export default function GeneralPage() {
|
|||||||
id: "timestamp",
|
id: "timestamp",
|
||||||
desc: true
|
desc: true
|
||||||
}}
|
}}
|
||||||
|
// Server-side pagination props
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
// Row expansion props
|
||||||
expandable={true}
|
expandable={true}
|
||||||
renderExpandedRow={renderExpandedRow}
|
renderExpandedRow={renderExpandedRow}
|
||||||
disabled={!isPaidUser(tierMatrix.accessLogs) || build === "oss"}
|
disabled={!isPaidUser(tierMatrix.accessLogs) || build === "oss"}
|
||||||
@@ -510,41 +645,3 @@ export default function GeneralPage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSampleAccessLogs(): QueryAccessAuditLogResponse["log"] {
|
|
||||||
const locations = ["US", "DE", "GB", "FR", "JP", "CA", "AU"];
|
|
||||||
const types = ["password", "pincode", "login", "whitelistedEmail", "ssh"];
|
|
||||||
const actors = [
|
|
||||||
"alice@example.com",
|
|
||||||
"bob@example.com",
|
|
||||||
"carol@example.com",
|
|
||||||
null
|
|
||||||
];
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const sevenDaysAgo = now - 7 * 24 * 60 * 60;
|
|
||||||
|
|
||||||
return Array.from({ length: 10 }, (_, i) => {
|
|
||||||
const action = Math.random() > 0.3;
|
|
||||||
const actor = actors[Math.floor(Math.random() * actors.length)];
|
|
||||||
|
|
||||||
return {
|
|
||||||
timestamp: Math.floor(
|
|
||||||
sevenDaysAgo + Math.random() * (now - sevenDaysAgo)
|
|
||||||
),
|
|
||||||
action,
|
|
||||||
orgId: "sample-org",
|
|
||||||
actorType: actor ? "user" : null,
|
|
||||||
actor,
|
|
||||||
actorId: actor ? `user-${i}` : null,
|
|
||||||
resourceId: Math.floor(Math.random() * 5) + 1,
|
|
||||||
resourceNiceId: `resource-${(i % 3) + 1}`,
|
|
||||||
resourceName: `Resource ${(i % 3) + 1}`,
|
|
||||||
ip: `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
|
|
||||||
location: locations[Math.floor(Math.random() * locations.length)],
|
|
||||||
userAgent: "Mozilla/5.0",
|
|
||||||
metadata: null,
|
|
||||||
type: types[Math.floor(Math.random() * types.length)]
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -10,17 +10,14 @@ import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
|||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||||
import { logQueries } from "@app/lib/queries";
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import type { QueryActionAuditLogResponse } from "@server/routers/auditLogs/types";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Key, User } from "lucide-react";
|
import { Key, User } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useMemo, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -31,8 +28,18 @@ export default function GeneralPage() {
|
|||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [isExporting, startTransition] = useTransition();
|
const [isExporting, startTransition] = useTransition();
|
||||||
|
const [filterAttributes, setFilterAttributes] = useState<{
|
||||||
|
actors: string[];
|
||||||
|
actions: string[];
|
||||||
|
}>({
|
||||||
|
actors: [],
|
||||||
|
actions: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter states - unified object for all filters
|
||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
action?: string;
|
action?: string;
|
||||||
actor?: string;
|
actor?: string;
|
||||||
@@ -41,21 +48,40 @@ export default function GeneralPage() {
|
|||||||
actor: searchParams.get("actor") || undefined
|
actor: searchParams.get("actor") || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Initialize page size from storage or default
|
||||||
const [pageSize, setPageSize] = useStoredPageSize("action-audit-logs", 20);
|
const [pageSize, setPageSize] = useStoredPageSize("action-audit-logs", 20);
|
||||||
|
|
||||||
|
// Set default date range to last 24 hours
|
||||||
const getDefaultDateRange = () => {
|
const getDefaultDateRange = () => {
|
||||||
|
// if the time is in the url params, use that instead
|
||||||
const startParam = searchParams.get("start");
|
const startParam = searchParams.get("start");
|
||||||
const endParam = searchParams.get("end");
|
const endParam = searchParams.get("end");
|
||||||
if (startParam && endParam) {
|
if (startParam && endParam) {
|
||||||
return {
|
return {
|
||||||
startDate: { date: new Date(startParam) },
|
startDate: {
|
||||||
endDate: { date: new Date(endParam) }
|
date: new Date(startParam)
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
date: new Date(endParam)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const lastWeek = getSevenDaysAgo();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startDate: { date: getSevenDaysAgo() },
|
startDate: {
|
||||||
endDate: { date: new Date() }
|
date: lastWeek
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
date: now
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -64,90 +90,78 @@ export default function GeneralPage() {
|
|||||||
endDate: DateTimeValue;
|
endDate: DateTimeValue;
|
||||||
}>(getDefaultDateRange());
|
}>(getDefaultDateRange());
|
||||||
|
|
||||||
const queryFilters = useMemo(() => {
|
// Trigger search with default values on component mount
|
||||||
let timeStart: string | undefined;
|
useEffect(() => {
|
||||||
let timeEnd: string | undefined;
|
if (build === "oss") {
|
||||||
|
return;
|
||||||
if (dateRange.startDate?.date) {
|
|
||||||
const dt = new Date(dateRange.startDate.date);
|
|
||||||
if (dateRange.startDate.time) {
|
|
||||||
const [h, m, s] = dateRange.startDate.time
|
|
||||||
.split(":")
|
|
||||||
.map(Number);
|
|
||||||
dt.setHours(h, m, s || 0);
|
|
||||||
}
|
}
|
||||||
timeStart = dt.toISOString();
|
const defaultRange = getDefaultDateRange();
|
||||||
}
|
queryDateTime(
|
||||||
|
defaultRange.startDate,
|
||||||
if (dateRange.endDate?.date) {
|
defaultRange.endDate,
|
||||||
const dt = new Date(dateRange.endDate.date);
|
0,
|
||||||
if (dateRange.endDate.time) {
|
pageSize
|
||||||
const [h, m, s] = dateRange.endDate.time.split(":").map(Number);
|
|
||||||
dt.setHours(h, m, s || 0);
|
|
||||||
} else {
|
|
||||||
const now = new Date();
|
|
||||||
dt.setHours(
|
|
||||||
now.getHours(),
|
|
||||||
now.getMinutes(),
|
|
||||||
now.getSeconds(),
|
|
||||||
now.getMilliseconds()
|
|
||||||
);
|
);
|
||||||
}
|
}, [orgId]); // Re-run if orgId changes
|
||||||
timeEnd = dt.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeStart,
|
|
||||||
timeEnd,
|
|
||||||
page: currentPage,
|
|
||||||
pageSize,
|
|
||||||
...filters
|
|
||||||
};
|
|
||||||
}, [dateRange, currentPage, pageSize, filters]);
|
|
||||||
|
|
||||||
const { data, isFetching, isLoading, refetch } = useQuery({
|
|
||||||
...logQueries.action({
|
|
||||||
orgId: orgId as string,
|
|
||||||
filters: queryFilters
|
|
||||||
}),
|
|
||||||
enabled: isPaidUser(tierMatrix.actionLogs) && build !== "oss"
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = isLoading ? generateSampleActionLogs() : (data?.log ?? []);
|
|
||||||
const totalCount = data?.pagination?.total ?? 0;
|
|
||||||
const filterAttributes = {
|
|
||||||
actors: data?.filterAttributes?.actors ?? []
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDateRangeChange = (
|
const handleDateRangeChange = (
|
||||||
startDate: DateTimeValue,
|
startDate: DateTimeValue,
|
||||||
endDate: DateTimeValue
|
endDate: DateTimeValue
|
||||||
) => {
|
) => {
|
||||||
setDateRange({ startDate, endDate });
|
setDateRange({ startDate, endDate });
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when filtering
|
||||||
|
// put the search params in the url for the time
|
||||||
updateUrlParamsForAllFilters({
|
updateUrlParamsForAllFilters({
|
||||||
start: startDate.date?.toISOString() || "",
|
start: startDate.date?.toISOString() || "",
|
||||||
end: endDate.date?.toISOString() || ""
|
end: endDate.date?.toISOString() || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryDateTime(startDate, endDate, 0, pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page changes
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
setCurrentPage(newPage);
|
setCurrentPage(newPage);
|
||||||
|
queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
newPage,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page size changes
|
||||||
const handlePageSizeChange = (newPageSize: number) => {
|
const handlePageSizeChange = (newPageSize: number) => {
|
||||||
setPageSize(newPageSize);
|
setPageSize(newPageSize);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when changing page size
|
||||||
|
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle filter changes generically
|
||||||
const handleFilterChange = (
|
const handleFilterChange = (
|
||||||
filterType: keyof typeof filters,
|
filterType: keyof typeof filters,
|
||||||
value: string | undefined
|
value: string | undefined
|
||||||
) => {
|
) => {
|
||||||
const newFilters = { ...filters, [filterType]: value };
|
// Create new filters object with updated value
|
||||||
|
const newFilters = {
|
||||||
|
...filters,
|
||||||
|
[filterType]: value
|
||||||
|
};
|
||||||
|
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when filtering
|
||||||
|
|
||||||
|
// Update URL params
|
||||||
updateUrlParamsForAllFilters(newFilters);
|
updateUrlParamsForAllFilters(newFilters);
|
||||||
|
|
||||||
|
// Trigger new query with updated filters (pass directly to avoid async state issues)
|
||||||
|
queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
0,
|
||||||
|
pageSize,
|
||||||
|
newFilters
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUrlParamsForAllFilters = (
|
const updateUrlParamsForAllFilters = (
|
||||||
@@ -169,8 +183,110 @@ export default function GeneralPage() {
|
|||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
router.replace(`?${params.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryDateTime = async (
|
||||||
|
startDate: DateTimeValue,
|
||||||
|
endDate: DateTimeValue,
|
||||||
|
page: number = currentPage,
|
||||||
|
size: number = pageSize,
|
||||||
|
filtersParam?: {
|
||||||
|
action?: string;
|
||||||
|
actor?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||||
|
if (!isPaidUser(tierMatrix.actionLogs)) {
|
||||||
|
console.log(
|
||||||
|
"Access denied: subscription inactive or license locked"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the provided filters or fall back to current state
|
||||||
|
const activeFilters = filtersParam || filters;
|
||||||
|
|
||||||
|
// Convert the date/time values to API parameters
|
||||||
|
const params: any = {
|
||||||
|
limit: size,
|
||||||
|
offset: page * size,
|
||||||
|
...activeFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startDate?.date) {
|
||||||
|
const startDateTime = new Date(startDate.date);
|
||||||
|
if (startDate.time) {
|
||||||
|
const [hours, minutes, seconds] = startDate.time
|
||||||
|
.split(":")
|
||||||
|
.map(Number);
|
||||||
|
startDateTime.setHours(hours, minutes, seconds || 0);
|
||||||
|
}
|
||||||
|
params.timeStart = startDateTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate?.date) {
|
||||||
|
const endDateTime = new Date(endDate.date);
|
||||||
|
if (endDate.time) {
|
||||||
|
const [hours, minutes, seconds] = endDate.time
|
||||||
|
.split(":")
|
||||||
|
.map(Number);
|
||||||
|
endDateTime.setHours(hours, minutes, seconds || 0);
|
||||||
|
} else {
|
||||||
|
// If no time is specified, set to NOW
|
||||||
|
const now = new Date();
|
||||||
|
endDateTime.setHours(
|
||||||
|
now.getHours(),
|
||||||
|
now.getMinutes(),
|
||||||
|
now.getSeconds(),
|
||||||
|
now.getMilliseconds()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
params.timeEnd = endDateTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.get(`/org/${orgId}/logs/action`, { params });
|
||||||
|
if (res.status === 200) {
|
||||||
|
setRows(res.data.data.log || []);
|
||||||
|
setTotalCount(res.data.data.pagination?.total || 0);
|
||||||
|
setFilterAttributes(res.data.data.filterAttributes);
|
||||||
|
console.log("Fetched logs:", res.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("Failed to filter logs"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
// Refresh data with current date range and pagination
|
||||||
|
await queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
currentPage,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const exportData = async () => {
|
const exportData = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Prepare query params for export
|
||||||
const params: any = {
|
const params: any = {
|
||||||
timeStart: dateRange.startDate?.date
|
timeStart: dateRange.startDate?.date
|
||||||
? new Date(dateRange.startDate.date).toISOString()
|
? new Date(dateRange.startDate.date).toISOString()
|
||||||
@@ -186,6 +302,7 @@ export default function GeneralPage() {
|
|||||||
params
|
params
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create a URL for the blob and trigger a download
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -203,6 +320,7 @@ export default function GeneralPage() {
|
|||||||
const data = error.response.data;
|
const data = error.response.data;
|
||||||
|
|
||||||
if (data instanceof Blob && data.type === "application/json") {
|
if (data instanceof Blob && data.type === "application/json") {
|
||||||
|
// Parse the Blob as JSON
|
||||||
const text = await data.text();
|
const text = await data.text();
|
||||||
const errorData = JSON.parse(text);
|
const errorData = JSON.parse(text);
|
||||||
apiErrorMessage = errorData.message;
|
apiErrorMessage = errorData.message;
|
||||||
@@ -219,7 +337,7 @@ export default function GeneralPage() {
|
|||||||
const columns: ColumnDef<any>[] = [
|
const columns: ColumnDef<any>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "timestamp",
|
accessorKey: "timestamp",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return t("timestamp");
|
return t("timestamp");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -234,16 +352,22 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "action",
|
accessorKey: "action",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("action")}</span>
|
<span>{t("action")}</span>
|
||||||
<ColumnFilter
|
<ColumnFilter
|
||||||
options={[]}
|
options={filterAttributes.actions.map((action) => ({
|
||||||
|
label:
|
||||||
|
action.charAt(0).toUpperCase() +
|
||||||
|
action.slice(1),
|
||||||
|
value: action
|
||||||
|
}))}
|
||||||
selectedValue={filters.action}
|
selectedValue={filters.action}
|
||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleFilterChange("action", value)
|
handleFilterChange("action", value)
|
||||||
}
|
}
|
||||||
|
// placeholder=""
|
||||||
searchPlaceholder="Search..."
|
searchPlaceholder="Search..."
|
||||||
emptyMessage="None found"
|
emptyMessage="None found"
|
||||||
/>
|
/>
|
||||||
@@ -261,7 +385,7 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "actor",
|
accessorKey: "actor",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("actor")}</span>
|
<span>{t("actor")}</span>
|
||||||
@@ -274,6 +398,7 @@ export default function GeneralPage() {
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleFilterChange("actor", value)
|
handleFilterChange("actor", value)
|
||||||
}
|
}
|
||||||
|
// placeholder=""
|
||||||
searchPlaceholder="Search..."
|
searchPlaceholder="Search..."
|
||||||
emptyMessage="None found"
|
emptyMessage="None found"
|
||||||
/>
|
/>
|
||||||
@@ -295,7 +420,7 @@ export default function GeneralPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "actorId",
|
accessorKey: "actorId",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return t("actorId");
|
return t("actorId");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -344,9 +469,12 @@ export default function GeneralPage() {
|
|||||||
title={t("actionLogs")}
|
title={t("actionLogs")}
|
||||||
searchPlaceholder={t("searchLogs")}
|
searchPlaceholder={t("searchLogs")}
|
||||||
searchColumn="action"
|
searchColumn="action"
|
||||||
onRefresh={() => refetch()}
|
onRefresh={refreshData}
|
||||||
isRefreshing={isFetching}
|
isRefreshing={isRefreshing}
|
||||||
onExport={() => startTransition(exportData)}
|
onExport={() => startTransition(exportData)}
|
||||||
|
// isExportDisabled={ // not disabling this because the user should be able to click the button and get the feedback about needing to upgrade the plan
|
||||||
|
// !isPaidUser(tierMatrix.logExport) || build === "oss"
|
||||||
|
// }
|
||||||
isExporting={isExporting}
|
isExporting={isExporting}
|
||||||
onDateRangeChange={handleDateRangeChange}
|
onDateRangeChange={handleDateRangeChange}
|
||||||
dateRange={{
|
dateRange={{
|
||||||
@@ -357,12 +485,14 @@ export default function GeneralPage() {
|
|||||||
id: "timestamp",
|
id: "timestamp",
|
||||||
desc: true
|
desc: true
|
||||||
}}
|
}}
|
||||||
|
// Server-side pagination props
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
// Row expansion props
|
||||||
expandable={true}
|
expandable={true}
|
||||||
renderExpandedRow={renderExpandedRow}
|
renderExpandedRow={renderExpandedRow}
|
||||||
disabled={!isPaidUser(tierMatrix.actionLogs) || build === "oss"}
|
disabled={!isPaidUser(tierMatrix.actionLogs) || build === "oss"}
|
||||||
@@ -370,39 +500,3 @@ export default function GeneralPage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSampleActionLogs(): QueryActionAuditLogResponse["log"] {
|
|
||||||
const actions = [
|
|
||||||
"createResource",
|
|
||||||
"deleteResource",
|
|
||||||
"updateResource",
|
|
||||||
"createSite",
|
|
||||||
"deleteSite",
|
|
||||||
"inviteUser",
|
|
||||||
"removeUser"
|
|
||||||
];
|
|
||||||
const actors = [
|
|
||||||
"alice@example.com",
|
|
||||||
"bob@example.com",
|
|
||||||
"carol@example.com"
|
|
||||||
];
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const sevenDaysAgo = now - 7 * 24 * 60 * 60;
|
|
||||||
|
|
||||||
return Array.from({ length: 10 }, (_, i) => {
|
|
||||||
const actor = actors[Math.floor(Math.random() * actors.length)];
|
|
||||||
|
|
||||||
return {
|
|
||||||
timestamp: Math.floor(
|
|
||||||
sevenDaysAgo + Math.random() * (now - sevenDaysAgo)
|
|
||||||
),
|
|
||||||
action: actions[Math.floor(Math.random() * actions.length)],
|
|
||||||
orgId: "sample-org",
|
|
||||||
actorType: "user",
|
|
||||||
actor,
|
|
||||||
actorId: `user-${i}`,
|
|
||||||
metadata: null
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,20 +9,26 @@ import { useEnvContext } from "@app/hooks/useEnvContext";
|
|||||||
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||||
import { toast } from "@app/hooks/useToast";
|
import { toast } from "@app/hooks/useToast";
|
||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient, formatAxiosError } from "@app/lib/api";
|
||||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||||
import { logQueries } from "@app/lib/queries";
|
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
import { tierMatrix } from "@server/lib/billing/tierMatrix";
|
||||||
import type { QueryConnectionAuditLogResponse } from "@server/routers/auditLogs/types";
|
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { ArrowUpRight, Laptop, User } from "lucide-react";
|
import { ArrowUpRight, Laptop, User } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useMemo, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
|
|
||||||
|
function formatBytes(bytes: number | null): string {
|
||||||
|
if (bytes === null || bytes === undefined) return "-";
|
||||||
|
if (bytes === 0) return "0 B";
|
||||||
|
const units = ["B", "KB", "MB", "GB", "TB"];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(1024));
|
||||||
|
const value = bytes / Math.pow(1024, i);
|
||||||
|
return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`;
|
||||||
|
}
|
||||||
|
|
||||||
function formatDuration(startedAt: number, endedAt: number | null): string {
|
function formatDuration(startedAt: number, endedAt: number | null): string {
|
||||||
if (endedAt === null || endedAt === undefined) return "Active";
|
if (endedAt === null || endedAt === undefined) return "Active";
|
||||||
@@ -48,8 +54,24 @@ export default function ConnectionLogsPage() {
|
|||||||
|
|
||||||
const { isPaidUser } = usePaidStatus();
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
|
||||||
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [isExporting, startTransition] = useTransition();
|
const [isExporting, startTransition] = useTransition();
|
||||||
|
const [filterAttributes, setFilterAttributes] = useState<{
|
||||||
|
protocols: string[];
|
||||||
|
destAddrs: string[];
|
||||||
|
clients: { id: number; name: string }[];
|
||||||
|
resources: { id: number; name: string | null }[];
|
||||||
|
users: { id: string; email: string | null }[];
|
||||||
|
}>({
|
||||||
|
protocols: [],
|
||||||
|
destAddrs: [],
|
||||||
|
clients: [],
|
||||||
|
resources: [],
|
||||||
|
users: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter states - unified object for all filters
|
||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
protocol?: string;
|
protocol?: string;
|
||||||
destAddr?: string;
|
destAddr?: string;
|
||||||
@@ -64,24 +86,43 @@ export default function ConnectionLogsPage() {
|
|||||||
userId: searchParams.get("userId") || undefined
|
userId: searchParams.get("userId") || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Initialize page size from storage or default
|
||||||
const [pageSize, setPageSize] = useStoredPageSize(
|
const [pageSize, setPageSize] = useStoredPageSize(
|
||||||
"connection-audit-logs",
|
"connection-audit-logs",
|
||||||
20
|
20
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Set default date range to last 7 days
|
||||||
const getDefaultDateRange = () => {
|
const getDefaultDateRange = () => {
|
||||||
|
// if the time is in the url params, use that instead
|
||||||
const startParam = searchParams.get("start");
|
const startParam = searchParams.get("start");
|
||||||
const endParam = searchParams.get("end");
|
const endParam = searchParams.get("end");
|
||||||
if (startParam && endParam) {
|
if (startParam && endParam) {
|
||||||
return {
|
return {
|
||||||
startDate: { date: new Date(startParam) },
|
startDate: {
|
||||||
endDate: { date: new Date(endParam) }
|
date: new Date(startParam)
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
date: new Date(endParam)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const lastWeek = getSevenDaysAgo();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startDate: { date: getSevenDaysAgo() },
|
startDate: {
|
||||||
endDate: { date: new Date() }
|
date: lastWeek
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
date: now
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -90,100 +131,78 @@ export default function ConnectionLogsPage() {
|
|||||||
endDate: DateTimeValue;
|
endDate: DateTimeValue;
|
||||||
}>(getDefaultDateRange());
|
}>(getDefaultDateRange());
|
||||||
|
|
||||||
const queryFilters = useMemo(() => {
|
// Trigger search with default values on component mount
|
||||||
let timeStart: string | undefined;
|
useEffect(() => {
|
||||||
let timeEnd: string | undefined;
|
if (build === "oss") {
|
||||||
|
return;
|
||||||
if (dateRange.startDate?.date) {
|
|
||||||
const dt = new Date(dateRange.startDate.date);
|
|
||||||
if (dateRange.startDate.time) {
|
|
||||||
const [h, m, s] = dateRange.startDate.time
|
|
||||||
.split(":")
|
|
||||||
.map(Number);
|
|
||||||
dt.setHours(h, m, s || 0);
|
|
||||||
}
|
}
|
||||||
timeStart = dt.toISOString();
|
const defaultRange = getDefaultDateRange();
|
||||||
}
|
queryDateTime(
|
||||||
|
defaultRange.startDate,
|
||||||
if (dateRange.endDate?.date) {
|
defaultRange.endDate,
|
||||||
const dt = new Date(dateRange.endDate.date);
|
0,
|
||||||
if (dateRange.endDate.time) {
|
pageSize
|
||||||
const [h, m, s] = dateRange.endDate.time.split(":").map(Number);
|
|
||||||
dt.setHours(h, m, s || 0);
|
|
||||||
} else {
|
|
||||||
const now = new Date();
|
|
||||||
dt.setHours(
|
|
||||||
now.getHours(),
|
|
||||||
now.getMinutes(),
|
|
||||||
now.getSeconds(),
|
|
||||||
now.getMilliseconds()
|
|
||||||
);
|
);
|
||||||
}
|
}, [orgId]); // Re-run if orgId changes
|
||||||
timeEnd = dt.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeStart,
|
|
||||||
timeEnd,
|
|
||||||
page: currentPage,
|
|
||||||
pageSize,
|
|
||||||
...filters,
|
|
||||||
clientId: filters.clientId ? Number(filters.clientId) : undefined,
|
|
||||||
siteResourceId: filters.siteResourceId
|
|
||||||
? Number(filters.siteResourceId)
|
|
||||||
: undefined
|
|
||||||
};
|
|
||||||
}, [dateRange, currentPage, pageSize, filters]);
|
|
||||||
|
|
||||||
const { data, isFetching, isLoading, refetch } = useQuery({
|
|
||||||
...logQueries.connection({
|
|
||||||
orgId: orgId as string,
|
|
||||||
filters: queryFilters
|
|
||||||
}),
|
|
||||||
enabled: isPaidUser(tierMatrix.connectionLogs) && build !== "oss"
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = isLoading
|
|
||||||
? generateSampleConnectionLogs()
|
|
||||||
: (data?.log ?? []);
|
|
||||||
const totalCount = data?.pagination?.total ?? 0;
|
|
||||||
const filterAttributes = data?.filterAttributes ?? {
|
|
||||||
protocols: [],
|
|
||||||
destAddrs: [],
|
|
||||||
clients: [],
|
|
||||||
resources: [],
|
|
||||||
users: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDateRangeChange = (
|
const handleDateRangeChange = (
|
||||||
startDate: DateTimeValue,
|
startDate: DateTimeValue,
|
||||||
endDate: DateTimeValue
|
endDate: DateTimeValue
|
||||||
) => {
|
) => {
|
||||||
setDateRange({ startDate, endDate });
|
setDateRange({ startDate, endDate });
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when filtering
|
||||||
|
// put the search params in the url for the time
|
||||||
updateUrlParamsForAllFilters({
|
updateUrlParamsForAllFilters({
|
||||||
start: startDate.date?.toISOString() || "",
|
start: startDate.date?.toISOString() || "",
|
||||||
end: endDate.date?.toISOString() || ""
|
end: endDate.date?.toISOString() || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryDateTime(startDate, endDate, 0, pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page changes
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
setCurrentPage(newPage);
|
setCurrentPage(newPage);
|
||||||
|
queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
newPage,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page size changes
|
||||||
const handlePageSizeChange = (newPageSize: number) => {
|
const handlePageSizeChange = (newPageSize: number) => {
|
||||||
setPageSize(newPageSize);
|
setPageSize(newPageSize);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when changing page size
|
||||||
|
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle filter changes generically
|
||||||
const handleFilterChange = (
|
const handleFilterChange = (
|
||||||
filterType: keyof typeof filters,
|
filterType: keyof typeof filters,
|
||||||
value: string | undefined
|
value: string | undefined
|
||||||
) => {
|
) => {
|
||||||
const newFilters = { ...filters, [filterType]: value };
|
// Create new filters object with updated value
|
||||||
|
const newFilters = {
|
||||||
|
...filters,
|
||||||
|
[filterType]: value
|
||||||
|
};
|
||||||
|
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when filtering
|
||||||
|
|
||||||
|
// Update URL params
|
||||||
updateUrlParamsForAllFilters(newFilters);
|
updateUrlParamsForAllFilters(newFilters);
|
||||||
|
|
||||||
|
// Trigger new query with updated filters (pass directly to avoid async state issues)
|
||||||
|
queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
0,
|
||||||
|
pageSize,
|
||||||
|
newFilters
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUrlParamsForAllFilters = (
|
const updateUrlParamsForAllFilters = (
|
||||||
@@ -205,8 +224,109 @@ export default function ConnectionLogsPage() {
|
|||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
router.replace(`?${params.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryDateTime = async (
|
||||||
|
startDate: DateTimeValue,
|
||||||
|
endDate: DateTimeValue,
|
||||||
|
page: number = currentPage,
|
||||||
|
size: number = pageSize,
|
||||||
|
filtersParam?: typeof filters
|
||||||
|
) => {
|
||||||
|
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||||
|
if (!isPaidUser(tierMatrix.connectionLogs)) {
|
||||||
|
console.log(
|
||||||
|
"Access denied: subscription inactive or license locked"
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the provided filters or fall back to current state
|
||||||
|
const activeFilters = filtersParam || filters;
|
||||||
|
|
||||||
|
// Convert the date/time values to API parameters
|
||||||
|
const params: any = {
|
||||||
|
limit: size,
|
||||||
|
offset: page * size,
|
||||||
|
...activeFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startDate?.date) {
|
||||||
|
const startDateTime = new Date(startDate.date);
|
||||||
|
if (startDate.time) {
|
||||||
|
const [hours, minutes, seconds] = startDate.time
|
||||||
|
.split(":")
|
||||||
|
.map(Number);
|
||||||
|
startDateTime.setHours(hours, minutes, seconds || 0);
|
||||||
|
}
|
||||||
|
params.timeStart = startDateTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate?.date) {
|
||||||
|
const endDateTime = new Date(endDate.date);
|
||||||
|
if (endDate.time) {
|
||||||
|
const [hours, minutes, seconds] = endDate.time
|
||||||
|
.split(":")
|
||||||
|
.map(Number);
|
||||||
|
endDateTime.setHours(hours, minutes, seconds || 0);
|
||||||
|
} else {
|
||||||
|
// If no time is specified, set to NOW
|
||||||
|
const now = new Date();
|
||||||
|
endDateTime.setHours(
|
||||||
|
now.getHours(),
|
||||||
|
now.getMinutes(),
|
||||||
|
now.getSeconds(),
|
||||||
|
now.getMilliseconds()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
params.timeEnd = endDateTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.get(`/org/${orgId}/logs/connection`, {
|
||||||
|
params
|
||||||
|
});
|
||||||
|
if (res.status === 200) {
|
||||||
|
setRows(res.data.data.log || []);
|
||||||
|
setTotalCount(res.data.data.pagination?.total || 0);
|
||||||
|
setFilterAttributes(res.data.data.filterAttributes);
|
||||||
|
console.log("Fetched connection logs:", res.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: formatAxiosError(error),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
// Refresh data with current date range and pagination
|
||||||
|
await queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
currentPage,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const exportData = async () => {
|
const exportData = async () => {
|
||||||
try {
|
try {
|
||||||
|
// Prepare query params for export
|
||||||
const params: any = {
|
const params: any = {
|
||||||
timeStart: dateRange.startDate?.date
|
timeStart: dateRange.startDate?.date
|
||||||
? new Date(dateRange.startDate.date).toISOString()
|
? new Date(dateRange.startDate.date).toISOString()
|
||||||
@@ -225,6 +345,7 @@ export default function ConnectionLogsPage() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Create a URL for the blob and trigger a download
|
||||||
const url = window.URL.createObjectURL(new Blob([response.data]));
|
const url = window.URL.createObjectURL(new Blob([response.data]));
|
||||||
const link = document.createElement("a");
|
const link = document.createElement("a");
|
||||||
link.href = url;
|
link.href = url;
|
||||||
@@ -242,6 +363,7 @@ export default function ConnectionLogsPage() {
|
|||||||
const data = error.response.data;
|
const data = error.response.data;
|
||||||
|
|
||||||
if (data instanceof Blob && data.type === "application/json") {
|
if (data instanceof Blob && data.type === "application/json") {
|
||||||
|
// Parse the Blob as JSON
|
||||||
const text = await data.text();
|
const text = await data.text();
|
||||||
const errorData = JSON.parse(text);
|
const errorData = JSON.parse(text);
|
||||||
apiErrorMessage = errorData.message;
|
apiErrorMessage = errorData.message;
|
||||||
@@ -258,7 +380,7 @@ export default function ConnectionLogsPage() {
|
|||||||
const columns: ColumnDef<any>[] = [
|
const columns: ColumnDef<any>[] = [
|
||||||
{
|
{
|
||||||
accessorKey: "startedAt",
|
accessorKey: "startedAt",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return t("timestamp");
|
return t("timestamp");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -273,7 +395,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "protocol",
|
accessorKey: "protocol",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("protocol")}</span>
|
<span>{t("protocol")}</span>
|
||||||
@@ -304,7 +426,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "resourceName",
|
accessorKey: "resourceName",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("resource")}</span>
|
<span>{t("resource")}</span>
|
||||||
@@ -345,7 +467,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "clientName",
|
accessorKey: "clientName",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("client")}</span>
|
<span>{t("client")}</span>
|
||||||
@@ -388,7 +510,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "userEmail",
|
accessorKey: "userEmail",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("user")}</span>
|
<span>{t("user")}</span>
|
||||||
@@ -421,7 +543,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "sourceAddr",
|
accessorKey: "sourceAddr",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return t("sourceAddress");
|
return t("sourceAddress");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -434,7 +556,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "destAddr",
|
accessorKey: "destAddr",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<span>{t("destinationAddress")}</span>
|
<span>{t("destinationAddress")}</span>
|
||||||
@@ -463,7 +585,7 @@ export default function ConnectionLogsPage() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
accessorKey: "duration",
|
accessorKey: "duration",
|
||||||
header: () => {
|
header: ({ column }) => {
|
||||||
return t("duration");
|
return t("duration");
|
||||||
},
|
},
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
@@ -484,6 +606,9 @@ export default function ConnectionLogsPage() {
|
|||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
<div className="grid grid-cols-1 md:grid-cols-3 gap-4 text-xs">
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||||
|
Connection Details
|
||||||
|
</div>*/}
|
||||||
<div>
|
<div>
|
||||||
<strong>Session ID:</strong>{" "}
|
<strong>Session ID:</strong>{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
@@ -508,6 +633,18 @@ export default function ConnectionLogsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
|
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||||
|
Resource & Site
|
||||||
|
</div>*/}
|
||||||
|
{/*<div>
|
||||||
|
<strong>Resource:</strong>{" "}
|
||||||
|
{row.resourceName ?? "-"}
|
||||||
|
{row.resourceNiceId && (
|
||||||
|
<span className="text-muted-foreground ml-1">
|
||||||
|
({row.resourceNiceId})
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>*/}
|
||||||
<div>
|
<div>
|
||||||
<strong>Client Endpoint:</strong>{" "}
|
<strong>Client Endpoint:</strong>{" "}
|
||||||
<span className="font-mono">
|
<span className="font-mono">
|
||||||
@@ -543,8 +680,30 @@ export default function ConnectionLogsPage() {
|
|||||||
<strong>Duration:</strong>{" "}
|
<strong>Duration:</strong>{" "}
|
||||||
{formatDuration(row.startedAt, row.endedAt)}
|
{formatDuration(row.startedAt, row.endedAt)}
|
||||||
</div>
|
</div>
|
||||||
|
{/*<div>
|
||||||
|
<strong>Resource ID:</strong>{" "}
|
||||||
|
{row.siteResourceId ?? "-"}
|
||||||
|
</div>*/}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2">
|
||||||
|
{/*<div className="flex items-center gap-1 font-semibold text-sm mb-1">
|
||||||
|
Client & Transfer
|
||||||
|
</div>*/}
|
||||||
|
{/*<div>
|
||||||
|
<strong>Bytes Sent (TX):</strong>{" "}
|
||||||
|
{formatBytes(row.bytesTx)}
|
||||||
|
</div>*/}
|
||||||
|
{/*<div>
|
||||||
|
<strong>Bytes Received (RX):</strong>{" "}
|
||||||
|
{formatBytes(row.bytesRx)}
|
||||||
|
</div>*/}
|
||||||
|
{/*<div>
|
||||||
|
<strong>Total Transfer:</strong>{" "}
|
||||||
|
{formatBytes(
|
||||||
|
(row.bytesTx ?? 0) + (row.bytesRx ?? 0)
|
||||||
|
)}
|
||||||
|
</div>*/}
|
||||||
</div>
|
</div>
|
||||||
<div className="space-y-2" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -565,8 +724,8 @@ export default function ConnectionLogsPage() {
|
|||||||
title={t("connectionLogs")}
|
title={t("connectionLogs")}
|
||||||
searchPlaceholder={t("searchLogs")}
|
searchPlaceholder={t("searchLogs")}
|
||||||
searchColumn="protocol"
|
searchColumn="protocol"
|
||||||
onRefresh={() => refetch()}
|
onRefresh={refreshData}
|
||||||
isRefreshing={isFetching}
|
isRefreshing={isRefreshing}
|
||||||
onExport={() => startTransition(exportData)}
|
onExport={() => startTransition(exportData)}
|
||||||
isExporting={isExporting}
|
isExporting={isExporting}
|
||||||
onDateRangeChange={handleDateRangeChange}
|
onDateRangeChange={handleDateRangeChange}
|
||||||
@@ -578,12 +737,14 @@ export default function ConnectionLogsPage() {
|
|||||||
id: "startedAt",
|
id: "startedAt",
|
||||||
desc: true
|
desc: true
|
||||||
}}
|
}}
|
||||||
|
// Server-side pagination props
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
pageSize={pageSize}
|
pageSize={pageSize}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
onPageSizeChange={handlePageSizeChange}
|
onPageSizeChange={handlePageSizeChange}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
|
// Row expansion props
|
||||||
expandable={true}
|
expandable={true}
|
||||||
renderExpandedRow={renderExpandedRow}
|
renderExpandedRow={renderExpandedRow}
|
||||||
disabled={
|
disabled={
|
||||||
@@ -593,49 +754,3 @@ export default function ConnectionLogsPage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSampleConnectionLogs(): QueryConnectionAuditLogResponse["log"] {
|
|
||||||
const protocols = ["tcp", "udp", "icmp"];
|
|
||||||
const destAddrs = [
|
|
||||||
"10.0.0.1:22",
|
|
||||||
"10.0.0.2:80",
|
|
||||||
"10.0.0.3:443",
|
|
||||||
"192.168.1.10:3306"
|
|
||||||
];
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const sevenDaysAgo = now - 7 * 24 * 60 * 60;
|
|
||||||
|
|
||||||
return Array.from({ length: 10 }, (_, i) => {
|
|
||||||
const startedAt = Math.floor(
|
|
||||||
sevenDaysAgo + Math.random() * (now - sevenDaysAgo)
|
|
||||||
);
|
|
||||||
const active = Math.random() > 0.3;
|
|
||||||
|
|
||||||
return {
|
|
||||||
sessionId: `session-${i}`,
|
|
||||||
siteResourceId: (i % 3) + 1,
|
|
||||||
orgId: "sample-org",
|
|
||||||
siteId: 1,
|
|
||||||
clientId: (i % 4) + 1,
|
|
||||||
clientEndpoint: `10.0.0.${i + 1}:51820`,
|
|
||||||
userId: i % 2 === 0 ? `user-${i}` : null,
|
|
||||||
sourceAddr: `192.168.1.${i + 1}:${40000 + i}`,
|
|
||||||
destAddr: destAddrs[Math.floor(Math.random() * destAddrs.length)],
|
|
||||||
protocol:
|
|
||||||
protocols[Math.floor(Math.random() * protocols.length)],
|
|
||||||
startedAt,
|
|
||||||
endedAt: active ? null : startedAt + Math.floor(Math.random() * 3600),
|
|
||||||
bytesTx: active ? null : Math.floor(Math.random() * 1024 * 1024),
|
|
||||||
bytesRx: active ? null : Math.floor(Math.random() * 1024 * 1024),
|
|
||||||
resourceName: `Resource ${(i % 3) + 1}`,
|
|
||||||
resourceNiceId: `resource-${(i % 3) + 1}`,
|
|
||||||
siteName: "Sample Site",
|
|
||||||
siteNiceId: "sample-site",
|
|
||||||
clientName: `Client ${(i % 4) + 1}`,
|
|
||||||
clientNiceId: `client-${(i % 4) + 1}`,
|
|
||||||
clientType: i % 2 === 0 ? "user" : "machine",
|
|
||||||
userEmail: i % 2 === 0 ? `user${i}@example.com` : null
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,17 +9,14 @@ import { toast } from "@app/hooks/useToast";
|
|||||||
import { createApiClient } from "@app/lib/api";
|
import { createApiClient } from "@app/lib/api";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
import { getSevenDaysAgo } from "@app/lib/getSevenDaysAgo";
|
||||||
import { logQueries } from "@app/lib/queries";
|
|
||||||
import { ColumnDef } from "@tanstack/react-table";
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react";
|
import { ArrowUpRight, Key, Lock, Unlock, User } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useMemo, useState, useTransition } from "react";
|
import { useEffect, useState, useTransition } from "react";
|
||||||
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
import { useStoredPageSize } from "@app/hooks/useStoredPageSize";
|
||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import type { QueryRequestAuditLogResponse } from "@server/routers/auditLogs/types";
|
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -28,11 +25,36 @@ export default function GeneralPage() {
|
|||||||
const { orgId } = useParams();
|
const { orgId } = useParams();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
const [rows, setRows] = useState<any[]>([]);
|
||||||
|
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||||
const [isExporting, startTransition] = useTransition();
|
const [isExporting, startTransition] = useTransition();
|
||||||
|
|
||||||
|
// Pagination state
|
||||||
|
const [totalCount, setTotalCount] = useState<number>(0);
|
||||||
const [currentPage, setCurrentPage] = useState<number>(0);
|
const [currentPage, setCurrentPage] = useState<number>(0);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
// Initialize page size from storage or default
|
||||||
const [pageSize, setPageSize] = useStoredPageSize("request-audit-logs", 20);
|
const [pageSize, setPageSize] = useStoredPageSize("request-audit-logs", 20);
|
||||||
|
|
||||||
|
const [filterAttributes, setFilterAttributes] = useState<{
|
||||||
|
actors: string[];
|
||||||
|
resources: {
|
||||||
|
id: number;
|
||||||
|
name: string | null;
|
||||||
|
}[];
|
||||||
|
locations: string[];
|
||||||
|
hosts: string[];
|
||||||
|
paths: string[];
|
||||||
|
}>({
|
||||||
|
actors: [],
|
||||||
|
resources: [],
|
||||||
|
locations: [],
|
||||||
|
hosts: [],
|
||||||
|
paths: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// Filter states - unified object for all filters
|
||||||
const [filters, setFilters] = useState<{
|
const [filters, setFilters] = useState<{
|
||||||
action?: string;
|
action?: string;
|
||||||
resourceId?: string;
|
resourceId?: string;
|
||||||
@@ -53,18 +75,32 @@ export default function GeneralPage() {
|
|||||||
path: searchParams.get("path") || undefined
|
path: searchParams.get("path") || undefined
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Set default date range to last 24 hours
|
||||||
const getDefaultDateRange = () => {
|
const getDefaultDateRange = () => {
|
||||||
|
// if the time is in the url params, use that instead
|
||||||
const startParam = searchParams.get("start");
|
const startParam = searchParams.get("start");
|
||||||
const endParam = searchParams.get("end");
|
const endParam = searchParams.get("end");
|
||||||
if (startParam && endParam) {
|
if (startParam && endParam) {
|
||||||
return {
|
return {
|
||||||
startDate: { date: new Date(startParam) },
|
startDate: {
|
||||||
endDate: { date: new Date(endParam) }
|
date: new Date(startParam)
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
date: new Date(endParam)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const now = new Date();
|
||||||
|
const lastWeek = getSevenDaysAgo();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
startDate: { date: getSevenDaysAgo() },
|
startDate: {
|
||||||
endDate: { date: new Date() }
|
date: lastWeek
|
||||||
|
},
|
||||||
|
endDate: {
|
||||||
|
date: now
|
||||||
|
}
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -73,97 +109,80 @@ export default function GeneralPage() {
|
|||||||
endDate: DateTimeValue;
|
endDate: DateTimeValue;
|
||||||
}>(getDefaultDateRange());
|
}>(getDefaultDateRange());
|
||||||
|
|
||||||
const queryFilters = useMemo(() => {
|
// Trigger search with default values on component mount
|
||||||
let timeStart: string | undefined;
|
useEffect(() => {
|
||||||
let timeEnd: string | undefined;
|
if (build === "oss") {
|
||||||
|
return;
|
||||||
if (dateRange.startDate?.date) {
|
|
||||||
const dt = new Date(dateRange.startDate.date);
|
|
||||||
if (dateRange.startDate.time) {
|
|
||||||
const [h, m, s] = dateRange.startDate.time
|
|
||||||
.split(":")
|
|
||||||
.map(Number);
|
|
||||||
dt.setHours(h, m, s || 0);
|
|
||||||
}
|
}
|
||||||
timeStart = dt.toISOString();
|
const defaultRange = getDefaultDateRange();
|
||||||
}
|
queryDateTime(
|
||||||
|
defaultRange.startDate,
|
||||||
if (dateRange.endDate?.date) {
|
defaultRange.endDate,
|
||||||
const dt = new Date(dateRange.endDate.date);
|
0,
|
||||||
if (dateRange.endDate.time) {
|
pageSize
|
||||||
const [h, m, s] = dateRange.endDate.time.split(":").map(Number);
|
|
||||||
dt.setHours(h, m, s || 0);
|
|
||||||
} else {
|
|
||||||
const now = new Date();
|
|
||||||
dt.setHours(
|
|
||||||
now.getHours(),
|
|
||||||
now.getMinutes(),
|
|
||||||
now.getSeconds(),
|
|
||||||
now.getMilliseconds()
|
|
||||||
);
|
);
|
||||||
}
|
}, [orgId]); // Re-run if orgId changes
|
||||||
timeEnd = dt.toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
timeStart,
|
|
||||||
timeEnd,
|
|
||||||
page: currentPage,
|
|
||||||
pageSize,
|
|
||||||
...filters,
|
|
||||||
resourceId: filters.resourceId
|
|
||||||
? Number(filters.resourceId)
|
|
||||||
: undefined
|
|
||||||
};
|
|
||||||
}, [dateRange, currentPage, pageSize, filters]);
|
|
||||||
|
|
||||||
const { data, isFetching, isLoading, refetch } = useQuery({
|
|
||||||
...logQueries.requests({
|
|
||||||
orgId: orgId as string,
|
|
||||||
filters: queryFilters
|
|
||||||
}),
|
|
||||||
enabled: build !== "oss"
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = isLoading ? generateSampleRequestLogs() : (data?.log ?? []);
|
|
||||||
const totalCount = data?.pagination?.total ?? 0;
|
|
||||||
const filterAttributes = data?.filterAttributes ?? {
|
|
||||||
actors: [],
|
|
||||||
resources: [],
|
|
||||||
locations: [],
|
|
||||||
hosts: [],
|
|
||||||
paths: []
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleDateRangeChange = (
|
const handleDateRangeChange = (
|
||||||
startDate: DateTimeValue,
|
startDate: DateTimeValue,
|
||||||
endDate: DateTimeValue
|
endDate: DateTimeValue
|
||||||
) => {
|
) => {
|
||||||
setDateRange({ startDate, endDate });
|
setDateRange({ startDate, endDate });
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when filtering
|
||||||
|
// put the search params in the url for the time
|
||||||
updateUrlParamsForAllFilters({
|
updateUrlParamsForAllFilters({
|
||||||
start: startDate.date?.toISOString() || "",
|
start: startDate.date?.toISOString() || "",
|
||||||
end: endDate.date?.toISOString() || ""
|
end: endDate.date?.toISOString() || ""
|
||||||
});
|
});
|
||||||
|
|
||||||
|
queryDateTime(startDate, endDate, 0, pageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page changes
|
||||||
const handlePageChange = (newPage: number) => {
|
const handlePageChange = (newPage: number) => {
|
||||||
setCurrentPage(newPage);
|
setCurrentPage(newPage);
|
||||||
|
queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
newPage,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle page size changes
|
||||||
const handlePageSizeChange = (newPageSize: number) => {
|
const handlePageSizeChange = (newPageSize: number) => {
|
||||||
setPageSize(newPageSize);
|
setPageSize(newPageSize);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when changing page size
|
||||||
|
queryDateTime(dateRange.startDate, dateRange.endDate, 0, newPageSize);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle filter changes generically
|
||||||
const handleFilterChange = (
|
const handleFilterChange = (
|
||||||
filterType: keyof typeof filters,
|
filterType: keyof typeof filters,
|
||||||
value: string | undefined
|
value: string | undefined
|
||||||
) => {
|
) => {
|
||||||
const newFilters = { ...filters, [filterType]: value };
|
console.log(`${filterType} filter changed:`, value);
|
||||||
|
|
||||||
|
// Create new filters object with updated value
|
||||||
|
const newFilters = {
|
||||||
|
...filters,
|
||||||
|
[filterType]: value
|
||||||
|
};
|
||||||
|
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
setCurrentPage(0);
|
setCurrentPage(0); // Reset to first page when filtering
|
||||||
|
|
||||||
|
// Update URL params
|
||||||
updateUrlParamsForAllFilters(newFilters);
|
updateUrlParamsForAllFilters(newFilters);
|
||||||
|
|
||||||
|
// Trigger new query with updated filters (pass directly to avoid async state issues)
|
||||||
|
queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
0,
|
||||||
|
pageSize,
|
||||||
|
newFilters
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateUrlParamsForAllFilters = (
|
const updateUrlParamsForAllFilters = (
|
||||||
@@ -185,6 +204,101 @@ export default function GeneralPage() {
|
|||||||
router.replace(`?${params.toString()}`, { scroll: false });
|
router.replace(`?${params.toString()}`, { scroll: false });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const queryDateTime = async (
|
||||||
|
startDate: DateTimeValue,
|
||||||
|
endDate: DateTimeValue,
|
||||||
|
page: number = currentPage,
|
||||||
|
size: number = pageSize,
|
||||||
|
filtersParam?: {
|
||||||
|
action?: string;
|
||||||
|
type?: string;
|
||||||
|
}
|
||||||
|
) => {
|
||||||
|
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Use the provided filters or fall back to current state
|
||||||
|
const activeFilters = filtersParam || filters;
|
||||||
|
|
||||||
|
// Convert the date/time values to API parameters
|
||||||
|
const params: any = {
|
||||||
|
limit: size,
|
||||||
|
offset: page * size,
|
||||||
|
...activeFilters
|
||||||
|
};
|
||||||
|
|
||||||
|
if (startDate?.date) {
|
||||||
|
const startDateTime = new Date(startDate.date);
|
||||||
|
if (startDate.time) {
|
||||||
|
const [hours, minutes, seconds] = startDate.time
|
||||||
|
.split(":")
|
||||||
|
.map(Number);
|
||||||
|
startDateTime.setHours(hours, minutes, seconds || 0);
|
||||||
|
}
|
||||||
|
params.timeStart = startDateTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate?.date) {
|
||||||
|
const endDateTime = new Date(endDate.date);
|
||||||
|
if (endDate.time) {
|
||||||
|
const [hours, minutes, seconds] = endDate.time
|
||||||
|
.split(":")
|
||||||
|
.map(Number);
|
||||||
|
endDateTime.setHours(hours, minutes, seconds || 0);
|
||||||
|
} else {
|
||||||
|
// If no time is specified, set to NOW
|
||||||
|
const now = new Date();
|
||||||
|
endDateTime.setHours(
|
||||||
|
now.getHours(),
|
||||||
|
now.getMinutes(),
|
||||||
|
now.getSeconds(),
|
||||||
|
now.getMilliseconds()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
params.timeEnd = endDateTime.toISOString();
|
||||||
|
}
|
||||||
|
|
||||||
|
const res = await api.get(`/org/${orgId}/logs/request`, { params });
|
||||||
|
if (res.status === 200) {
|
||||||
|
setRows(res.data.data.log || []);
|
||||||
|
setTotalCount(res.data.data.pagination?.total || 0);
|
||||||
|
setFilterAttributes(res.data.data.filterAttributes);
|
||||||
|
console.log("Fetched logs:", res.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("Failed to filter logs"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const refreshData = async () => {
|
||||||
|
console.log("Data refreshed");
|
||||||
|
setIsRefreshing(true);
|
||||||
|
try {
|
||||||
|
// Refresh data with current date range and pagination
|
||||||
|
await queryDateTime(
|
||||||
|
dateRange.startDate,
|
||||||
|
dateRange.endDate,
|
||||||
|
currentPage,
|
||||||
|
pageSize
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
toast({
|
||||||
|
title: t("error"),
|
||||||
|
description: t("refreshError"),
|
||||||
|
variant: "destructive"
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
setIsRefreshing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const exportData = async () => {
|
const exportData = async () => {
|
||||||
try {
|
try {
|
||||||
// Prepare query params for export
|
// Prepare query params for export
|
||||||
@@ -667,8 +781,8 @@ export default function GeneralPage() {
|
|||||||
title={t("requestLogs")}
|
title={t("requestLogs")}
|
||||||
searchPlaceholder={t("searchLogs")}
|
searchPlaceholder={t("searchLogs")}
|
||||||
searchColumn="host"
|
searchColumn="host"
|
||||||
onRefresh={() => refetch()}
|
onRefresh={refreshData}
|
||||||
isRefreshing={isFetching}
|
isRefreshing={isRefreshing}
|
||||||
onExport={() => startTransition(exportData)}
|
onExport={() => startTransition(exportData)}
|
||||||
isExporting={isExporting}
|
isExporting={isExporting}
|
||||||
onDateRangeChange={handleDateRangeChange}
|
onDateRangeChange={handleDateRangeChange}
|
||||||
@@ -680,6 +794,7 @@ export default function GeneralPage() {
|
|||||||
id: "timestamp",
|
id: "timestamp",
|
||||||
desc: true
|
desc: true
|
||||||
}}
|
}}
|
||||||
|
// Server-side pagination props
|
||||||
totalCount={totalCount}
|
totalCount={totalCount}
|
||||||
currentPage={currentPage}
|
currentPage={currentPage}
|
||||||
onPageChange={handlePageChange}
|
onPageChange={handlePageChange}
|
||||||
@@ -693,63 +808,3 @@ export default function GeneralPage() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function generateSampleRequestLogs(): QueryRequestAuditLogResponse["log"] {
|
|
||||||
const methods = ["GET", "POST", "PUT", "DELETE", "PATCH"];
|
|
||||||
const paths = [
|
|
||||||
"/api/v1/users",
|
|
||||||
"/dashboard",
|
|
||||||
"/settings",
|
|
||||||
"/health",
|
|
||||||
"/metrics"
|
|
||||||
];
|
|
||||||
const hosts = ["app.example.com", "api.example.com", "admin.example.com"];
|
|
||||||
const locations = ["US", "DE", "GB", "FR", "JP", "CA", "AU"];
|
|
||||||
const allowedReasons = [100, 101, 102, 103, 104, 105, 106, 107, 108];
|
|
||||||
const deniedReasons = [201, 202, 203, 204, 205, 299];
|
|
||||||
const actors = [
|
|
||||||
"alice@example.com",
|
|
||||||
"bob@example.com",
|
|
||||||
"carol@example.com",
|
|
||||||
null
|
|
||||||
];
|
|
||||||
|
|
||||||
const now = Math.floor(Date.now() / 1000);
|
|
||||||
const sevenDaysAgo = now - 7 * 24 * 60 * 60;
|
|
||||||
|
|
||||||
return Array.from({ length: 10 }, (_, i) => {
|
|
||||||
const action = Math.random() > 0.3;
|
|
||||||
const reason = action
|
|
||||||
? allowedReasons[Math.floor(Math.random() * allowedReasons.length)]
|
|
||||||
: deniedReasons[Math.floor(Math.random() * deniedReasons.length)];
|
|
||||||
const actor = actors[Math.floor(Math.random() * actors.length)];
|
|
||||||
|
|
||||||
return {
|
|
||||||
timestamp: Math.floor(
|
|
||||||
sevenDaysAgo + Math.random() * (now - sevenDaysAgo)
|
|
||||||
),
|
|
||||||
action,
|
|
||||||
reason,
|
|
||||||
orgId: "sample-org",
|
|
||||||
actorType: actor ? "user" : null,
|
|
||||||
actor,
|
|
||||||
actorId: actor ? `user-${i}` : null,
|
|
||||||
resourceId: Math.floor(Math.random() * 5) + 1,
|
|
||||||
siteResourceId: null,
|
|
||||||
resourceNiceId: `resource-${(i % 3) + 1}`,
|
|
||||||
resourceName: `Resource ${(i % 3) + 1}`,
|
|
||||||
ip: `${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}.${Math.floor(Math.random() * 255)}`,
|
|
||||||
location: locations[Math.floor(Math.random() * locations.length)],
|
|
||||||
userAgent: "Mozilla/5.0",
|
|
||||||
metadata: null,
|
|
||||||
headers: null,
|
|
||||||
query: null,
|
|
||||||
originalRequestURL: null,
|
|
||||||
scheme: "https",
|
|
||||||
host: hosts[Math.floor(Math.random() * hosts.length)],
|
|
||||||
path: paths[Math.floor(Math.random() * paths.length)],
|
|
||||||
method: methods[Math.floor(Math.random() * methods.length)],
|
|
||||||
tls: true
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -36,35 +36,53 @@ import { useState } from "react";
|
|||||||
import { SwitchInput } from "@app/components/SwitchInput";
|
import { SwitchInput } from "@app/components/SwitchInput";
|
||||||
import { ExternalLink } from "lucide-react";
|
import { ExternalLink } from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
|
import { useOrgContext } from "@app/hooks/useOrgContext";
|
||||||
|
import { usePaidStatus } from "@app/hooks/usePaidStatus";
|
||||||
|
import { tierMatrix, TierFeature } from "@server/lib/billing/tierMatrix";
|
||||||
|
import { Button as ButtonUI } from "@/components/ui/button";
|
||||||
|
import { PaidFeaturesAlert } from "@app/components/PaidFeaturesAlert";
|
||||||
|
|
||||||
const GeneralFormSchema = z.object({
|
const GeneralFormSchema = z.object({
|
||||||
name: z.string().nonempty("Name is required"),
|
name: z.string().nonempty("Name is required"),
|
||||||
niceId: z.string().min(1).max(255).optional(),
|
niceId: z.string().min(1).max(255).optional(),
|
||||||
dockerSocketEnabled: z.boolean().optional()
|
dockerSocketEnabled: z.boolean().optional(),
|
||||||
|
autoUpdateEnabled: z.boolean().optional(),
|
||||||
|
autoUpdateOverrideOrg: z.boolean().optional()
|
||||||
});
|
});
|
||||||
|
|
||||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||||
|
|
||||||
export default function GeneralPage() {
|
export default function GeneralPage() {
|
||||||
const { site, updateSite } = useSiteContext();
|
const { site, updateSite } = useSiteContext();
|
||||||
|
const { org } = useOrgContext();
|
||||||
|
|
||||||
const { env } = useEnvContext();
|
const { env } = useEnvContext();
|
||||||
const api = createApiClient(useEnvContext());
|
const api = createApiClient(useEnvContext());
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const t = useTranslations();
|
const t = useTranslations();
|
||||||
const { toast } = useToast();
|
const { toast } = useToast();
|
||||||
|
const { isPaidUser } = usePaidStatus();
|
||||||
|
const hasAutoUpdateFeature = isPaidUser(
|
||||||
|
tierMatrix[TierFeature.NewtAutoUpdate]
|
||||||
|
);
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
const [activeCidrTagIndex, setActiveCidrTagIndex] = useState<number | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const orgAutoUpdate = org.org.settingsEnableGlobalNewtAutoUpdate ?? false;
|
||||||
|
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
resolver: zodResolver(GeneralFormSchema),
|
resolver: zodResolver(GeneralFormSchema),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
name: site?.name,
|
name: site?.name,
|
||||||
niceId: site?.niceId || "",
|
niceId: site?.niceId || "",
|
||||||
dockerSocketEnabled: site?.dockerSocketEnabled ?? false
|
dockerSocketEnabled: site?.dockerSocketEnabled ?? false,
|
||||||
|
autoUpdateEnabled: site?.autoUpdateOverrideOrg
|
||||||
|
? (site?.autoUpdateEnabled ?? false)
|
||||||
|
: orgAutoUpdate,
|
||||||
|
autoUpdateOverrideOrg: site?.autoUpdateOverrideOrg ?? false
|
||||||
},
|
},
|
||||||
mode: "onChange"
|
mode: "onChange"
|
||||||
});
|
});
|
||||||
@@ -76,13 +94,17 @@ export default function GeneralPage() {
|
|||||||
await api.post(`/site/${site?.siteId}`, {
|
await api.post(`/site/${site?.siteId}`, {
|
||||||
name: data.name,
|
name: data.name,
|
||||||
niceId: data.niceId,
|
niceId: data.niceId,
|
||||||
dockerSocketEnabled: data.dockerSocketEnabled
|
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||||
|
autoUpdateEnabled: data.autoUpdateEnabled,
|
||||||
|
autoUpdateOverrideOrg: data.autoUpdateOverrideOrg
|
||||||
});
|
});
|
||||||
|
|
||||||
updateSite({
|
updateSite({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
niceId: data.niceId,
|
niceId: data.niceId,
|
||||||
dockerSocketEnabled: data.dockerSocketEnabled
|
dockerSocketEnabled: data.dockerSocketEnabled,
|
||||||
|
autoUpdateEnabled: data.autoUpdateEnabled,
|
||||||
|
autoUpdateOverrideOrg: data.autoUpdateOverrideOrg
|
||||||
});
|
});
|
||||||
|
|
||||||
if (data.niceId && data.niceId !== site?.niceId) {
|
if (data.niceId && data.niceId !== site?.niceId) {
|
||||||
@@ -199,7 +221,9 @@ export default function GeneralPage() {
|
|||||||
{t.rich(
|
{t.rich(
|
||||||
"enableDockerSocketDescription",
|
"enableDockerSocketDescription",
|
||||||
{
|
{
|
||||||
docsLink: (chunks) => (
|
docsLink: (
|
||||||
|
chunks
|
||||||
|
) => (
|
||||||
<a
|
<a
|
||||||
href="https://docs.pangolin.net/manage/sites/configure-site#docker-socket-integration"
|
href="https://docs.pangolin.net/manage/sites/configure-site#docker-socket-integration"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -217,6 +241,80 @@ export default function GeneralPage() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<PaidFeaturesAlert
|
||||||
|
tiers={tierMatrix.newtAutoUpdate}
|
||||||
|
/>
|
||||||
|
{site && site.type === "newt" && (
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="autoUpdateEnabled"
|
||||||
|
render={({ field }) => {
|
||||||
|
const isOverriding = form.watch(
|
||||||
|
"autoUpdateOverrideOrg"
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<FormItem>
|
||||||
|
<FormControl>
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
<SwitchInput
|
||||||
|
id="auto-update-enabled"
|
||||||
|
label={t(
|
||||||
|
"siteAutoUpdateLabel"
|
||||||
|
)}
|
||||||
|
checked={
|
||||||
|
field.value
|
||||||
|
}
|
||||||
|
onCheckedChange={(
|
||||||
|
checked
|
||||||
|
) => {
|
||||||
|
field.onChange(
|
||||||
|
checked
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"autoUpdateOverrideOrg",
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
disabled={
|
||||||
|
!hasAutoUpdateFeature
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
{isOverriding && (
|
||||||
|
<ButtonUI
|
||||||
|
type="button"
|
||||||
|
variant="link"
|
||||||
|
size="sm"
|
||||||
|
className="h-auto p-0 pb-2 text-xs"
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue(
|
||||||
|
"autoUpdateOverrideOrg",
|
||||||
|
false
|
||||||
|
);
|
||||||
|
form.setValue(
|
||||||
|
"autoUpdateEnabled",
|
||||||
|
orgAutoUpdate
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{t(
|
||||||
|
"siteAutoUpdateResetToOrg"
|
||||||
|
)}
|
||||||
|
</ButtonUI>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</FormControl>
|
||||||
|
<FormDescription>
|
||||||
|
{t(
|
||||||
|
"siteAutoUpdateDescription"
|
||||||
|
)}
|
||||||
|
</FormDescription>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
</SettingsSectionForm>
|
</SettingsSectionForm>
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import SiteProvider from "@app/providers/SiteProvider";
|
import SiteProvider from "@app/providers/SiteProvider";
|
||||||
|
import OrgProvider from "@app/providers/OrgProvider";
|
||||||
import { internal } from "@app/lib/api";
|
import { internal } from "@app/lib/api";
|
||||||
import { GetSiteResponse } from "@server/routers/site";
|
import { GetSiteResponse } from "@server/routers/site";
|
||||||
|
import { GetOrgResponse } from "@server/routers/org";
|
||||||
import { AxiosResponse } from "axios";
|
import { AxiosResponse } from "axios";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
import { authCookieHeader } from "@app/lib/api/cookies";
|
import { authCookieHeader } from "@app/lib/api/cookies";
|
||||||
@@ -35,6 +37,17 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||||||
redirect(`/${params.orgId}/settings/sites`);
|
redirect(`/${params.orgId}/settings/sites`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let org = null;
|
||||||
|
try {
|
||||||
|
const res = await internal.get<AxiosResponse<GetOrgResponse>>(
|
||||||
|
`/org/${params.orgId}`,
|
||||||
|
await authCookieHeader()
|
||||||
|
);
|
||||||
|
org = res.data.data;
|
||||||
|
} catch {
|
||||||
|
redirect(`/${params.orgId}/settings/sites`);
|
||||||
|
}
|
||||||
|
|
||||||
const t = await getTranslations();
|
const t = await getTranslations();
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
@@ -64,10 +77,14 @@ export default async function SettingsLayout(props: SettingsLayoutProps) {
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<SiteProvider site={site}>
|
<SiteProvider site={site}>
|
||||||
|
<OrgProvider org={org}>
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<SiteInfoCard />
|
<SiteInfoCard />
|
||||||
<HorizontalTabs items={navItems}>{children}</HorizontalTabs>
|
<HorizontalTabs items={navItems}>
|
||||||
|
{children}
|
||||||
|
</HorizontalTabs>
|
||||||
</div>
|
</div>
|
||||||
|
</OrgProvider>
|
||||||
</SiteProvider>
|
</SiteProvider>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -28,7 +28,6 @@ import {
|
|||||||
ChevronRight,
|
ChevronRight,
|
||||||
Download,
|
Download,
|
||||||
Loader,
|
Loader,
|
||||||
LoaderIcon,
|
|
||||||
RefreshCw
|
RefreshCw
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
@@ -428,7 +427,7 @@ export function LogDataTable<TData, TValue>({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent className="relative">
|
<CardContent>
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
{table.getHeaderGroups().map((headerGroup) => (
|
{table.getHeaderGroups().map((headerGroup) => (
|
||||||
@@ -536,19 +535,6 @@ export function LogDataTable<TData, TValue>({
|
|||||||
)}
|
)}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
|
|
||||||
{isLoading && (
|
|
||||||
<>
|
|
||||||
<div className="backdrop-blur-[3px] z-10 absolute inset-0 top-10"></div>
|
|
||||||
<div className="absolute z-20 left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2 border border-border rounded-md bg-muted">
|
|
||||||
<div className="flex items-center gap-2 p-6">
|
|
||||||
<LoaderIcon className="size-4 animate-spin" />
|
|
||||||
{t("loadingEllipsis")}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<DataTablePagination
|
<DataTablePagination
|
||||||
table={table}
|
table={table}
|
||||||
|
|||||||
@@ -45,7 +45,16 @@ export function SwitchInput({
|
|||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="flex items-center space-x-2 mb-2">
|
<div className="flex items-center space-x-2 mb-2">
|
||||||
{label && <Label htmlFor={id}>{label}</Label>}
|
{label && (
|
||||||
|
<Label
|
||||||
|
htmlFor={id}
|
||||||
|
className={
|
||||||
|
disabled ? "opacity-50 cursor-not-allowed" : ""
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</Label>
|
||||||
|
)}
|
||||||
<Switch
|
<Switch
|
||||||
id={id}
|
id={id}
|
||||||
checked={checked}
|
checked={checked}
|
||||||
|
|||||||
@@ -1,25 +1,17 @@
|
|||||||
import { build } from "@server/build";
|
import { build } from "@server/build";
|
||||||
import { StatusHistoryResponse } from "@server/lib/statusHistory";
|
|
||||||
import type { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
|
||||||
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
|
import type { QueryRequestAnalyticsResponse } from "@server/routers/auditLogs";
|
||||||
import type {
|
|
||||||
QueryAccessAuditLogResponse,
|
|
||||||
QueryActionAuditLogResponse,
|
|
||||||
QueryConnectionAuditLogResponse,
|
|
||||||
QueryRequestAuditLogResponse
|
|
||||||
} from "@server/routers/auditLogs/types";
|
|
||||||
import type { ListClientsResponse } from "@server/routers/client";
|
import type { ListClientsResponse } from "@server/routers/client";
|
||||||
import type {
|
import type {
|
||||||
GetDNSRecordsResponse,
|
ListDomainsResponse,
|
||||||
ListDomainsResponse
|
GetDNSRecordsResponse
|
||||||
} from "@server/routers/domain";
|
} from "@server/routers/domain";
|
||||||
import type { GetDomainResponse } from "@server/routers/domain/getDomain";
|
import type { GetDomainResponse } from "@server/routers/domain/getDomain";
|
||||||
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
|
|
||||||
import type {
|
import type {
|
||||||
GetResourceWhitelistResponse,
|
GetResourceWhitelistResponse,
|
||||||
ListResourceNamesResponse,
|
ListResourceNamesResponse,
|
||||||
ListResourcesResponse
|
ListResourcesResponse
|
||||||
} from "@server/routers/resource";
|
} from "@server/routers/resource";
|
||||||
|
import type { ListAlertRulesResponse } from "@server/routers/alertRule/types";
|
||||||
import type { ListRolesResponse } from "@server/routers/role";
|
import type { ListRolesResponse } from "@server/routers/role";
|
||||||
import type { ListSitesResponse } from "@server/routers/site";
|
import type { ListSitesResponse } from "@server/routers/site";
|
||||||
import type {
|
import type {
|
||||||
@@ -39,6 +31,8 @@ import type { AxiosResponse } from "axios";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
import { remote } from "./api";
|
import { remote } from "./api";
|
||||||
import { durationToMs } from "./durationToMs";
|
import { durationToMs } from "./durationToMs";
|
||||||
|
import { ListHealthChecksResponse } from "@server/routers/healthChecks/types";
|
||||||
|
import { StatusHistoryResponse } from "@server/lib/statusHistory";
|
||||||
import type { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
import type { ListOrgLabelsResponse } from "@server/routers/labels/types";
|
||||||
|
|
||||||
export type ProductUpdate = {
|
export type ProductUpdate = {
|
||||||
@@ -595,111 +589,7 @@ export const logAnalyticsFiltersSchema = z.object({
|
|||||||
resourceId: z.coerce.number().optional().catch(undefined)
|
resourceId: z.coerce.number().optional().catch(undefined)
|
||||||
});
|
});
|
||||||
|
|
||||||
export type LogAnalyticsFilters = z.output<typeof logAnalyticsFiltersSchema>;
|
export type LogAnalyticsFilters = z.TypeOf<typeof logAnalyticsFiltersSchema>;
|
||||||
|
|
||||||
export const httpLogsFiltersSchema = z.object({
|
|
||||||
timeStart: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
|
||||||
error: "timeStart must be a valid ISO date string"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.catch(undefined),
|
|
||||||
timeEnd: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
|
||||||
error: "timeEnd must be a valid ISO date string"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.catch(undefined),
|
|
||||||
page: z.coerce.number().optional().catch(0).default(0),
|
|
||||||
pageSize: z.coerce.number().optional().catch(20).default(20),
|
|
||||||
resourceId: z.coerce.number().optional().catch(undefined),
|
|
||||||
action: z.string().optional().catch(undefined),
|
|
||||||
host: z.string().optional().catch(undefined),
|
|
||||||
location: z.string().optional().catch(undefined),
|
|
||||||
actor: z.string().optional().catch(undefined),
|
|
||||||
method: z.string().optional().catch(undefined),
|
|
||||||
reason: z.string().optional().catch(undefined),
|
|
||||||
path: z.string().optional().catch(undefined)
|
|
||||||
});
|
|
||||||
|
|
||||||
export type HttpLogFilters = z.output<typeof httpLogsFiltersSchema>;
|
|
||||||
|
|
||||||
export const accessLogsFiltersSchema = z.object({
|
|
||||||
timeStart: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
|
||||||
error: "timeStart must be a valid ISO date string"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.catch(undefined),
|
|
||||||
timeEnd: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
|
||||||
error: "timeEnd must be a valid ISO date string"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.catch(undefined),
|
|
||||||
page: z.coerce.number().optional().catch(0).default(0),
|
|
||||||
pageSize: z.coerce.number().optional().catch(20).default(20),
|
|
||||||
resourceId: z.coerce.number().optional().catch(undefined),
|
|
||||||
action: z.string().optional().catch(undefined),
|
|
||||||
location: z.string().optional().catch(undefined),
|
|
||||||
actor: z.string().optional().catch(undefined),
|
|
||||||
type: z.string().optional().catch(undefined)
|
|
||||||
});
|
|
||||||
|
|
||||||
export type AccessLogFilters = z.output<typeof accessLogsFiltersSchema>;
|
|
||||||
|
|
||||||
export const actionLogsFiltersSchema = z.object({
|
|
||||||
timeStart: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
|
||||||
error: "timeStart must be a valid ISO date string"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.catch(undefined),
|
|
||||||
timeEnd: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
|
||||||
error: "timeEnd must be a valid ISO date string"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.catch(undefined),
|
|
||||||
page: z.coerce.number().optional().catch(0).default(0),
|
|
||||||
pageSize: z.coerce.number().optional().catch(20).default(20),
|
|
||||||
action: z.string().optional().catch(undefined),
|
|
||||||
actor: z.string().optional().catch(undefined)
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ActionLogFilters = z.output<typeof actionLogsFiltersSchema>;
|
|
||||||
|
|
||||||
export const connectionLogsFiltersSchema = z.object({
|
|
||||||
timeStart: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
|
||||||
error: "timeStart must be a valid ISO date string"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.catch(undefined),
|
|
||||||
timeEnd: z
|
|
||||||
.string()
|
|
||||||
.refine((val) => !isNaN(Date.parse(val)), {
|
|
||||||
error: "timeEnd must be a valid ISO date string"
|
|
||||||
})
|
|
||||||
.optional()
|
|
||||||
.catch(undefined),
|
|
||||||
page: z.coerce.number().optional().catch(0).default(0),
|
|
||||||
pageSize: z.coerce.number().optional().catch(20).default(20),
|
|
||||||
protocol: z.string().optional().catch(undefined),
|
|
||||||
destAddr: z.string().optional().catch(undefined),
|
|
||||||
clientId: z.coerce.number().optional().catch(undefined),
|
|
||||||
siteResourceId: z.coerce.number().optional().catch(undefined),
|
|
||||||
userId: z.string().optional().catch(undefined)
|
|
||||||
});
|
|
||||||
|
|
||||||
export type ConnectionLogFilters = z.output<typeof connectionLogsFiltersSchema>;
|
|
||||||
|
|
||||||
export const logQueries = {
|
export const logQueries = {
|
||||||
requestAnalytics: ({
|
requestAnalytics: ({
|
||||||
@@ -710,7 +600,7 @@ export const logQueries = {
|
|||||||
filters: LogAnalyticsFilters;
|
filters: LogAnalyticsFilters;
|
||||||
}) =>
|
}) =>
|
||||||
queryOptions({
|
queryOptions({
|
||||||
queryKey: ["REQUEST_LOGS", orgId, "ANALYTICS", filters] as const,
|
queryKey: ["REQUEST_LOG_ANALYTICS", orgId, filters] as const,
|
||||||
queryFn: async ({ signal, meta }) => {
|
queryFn: async ({ signal, meta }) => {
|
||||||
const res = await meta!.api.get<
|
const res = await meta!.api.get<
|
||||||
AxiosResponse<QueryRequestAnalyticsResponse>
|
AxiosResponse<QueryRequestAnalyticsResponse>
|
||||||
@@ -726,124 +616,6 @@ export const logQueries = {
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}),
|
|
||||||
|
|
||||||
requests: ({
|
|
||||||
orgId,
|
|
||||||
filters
|
|
||||||
}: {
|
|
||||||
orgId: string;
|
|
||||||
filters: HttpLogFilters;
|
|
||||||
}) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ["REQUEST_LOGS", orgId, "ALL", filters] as const,
|
|
||||||
queryFn: async ({ signal, meta }) => {
|
|
||||||
const { page, pageSize, ...rest } = filters;
|
|
||||||
const res = await meta!.api.get<
|
|
||||||
AxiosResponse<QueryRequestAuditLogResponse>
|
|
||||||
>(`/org/${orgId}/logs/request`, {
|
|
||||||
params: {
|
|
||||||
...rest,
|
|
||||||
limit: pageSize,
|
|
||||||
offset: page * pageSize
|
|
||||||
},
|
|
||||||
signal
|
|
||||||
});
|
|
||||||
return res.data.data;
|
|
||||||
},
|
|
||||||
refetchInterval: (query) => {
|
|
||||||
if (query.state.data) {
|
|
||||||
return durationToMs(30, "seconds");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
access: ({ orgId, filters }: { orgId: string; filters: AccessLogFilters }) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ["ACCESS_LOGS", orgId, "ALL", filters] as const,
|
|
||||||
queryFn: async ({ signal, meta }) => {
|
|
||||||
const { page, pageSize, ...rest } = filters;
|
|
||||||
const res = await meta!.api.get<
|
|
||||||
AxiosResponse<QueryAccessAuditLogResponse>
|
|
||||||
>(`/org/${orgId}/logs/access`, {
|
|
||||||
params: {
|
|
||||||
...rest,
|
|
||||||
limit: pageSize,
|
|
||||||
offset: page * pageSize
|
|
||||||
},
|
|
||||||
signal
|
|
||||||
});
|
|
||||||
return res.data.data;
|
|
||||||
},
|
|
||||||
refetchInterval: (query) => {
|
|
||||||
if (query.state.data) {
|
|
||||||
return durationToMs(30, "seconds");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
action: ({
|
|
||||||
orgId,
|
|
||||||
filters
|
|
||||||
}: {
|
|
||||||
orgId: string;
|
|
||||||
filters: ActionLogFilters;
|
|
||||||
}) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ["ACTION_LOGS", orgId, "ALL", filters] as const,
|
|
||||||
queryFn: async ({ signal, meta }) => {
|
|
||||||
const { page, pageSize, ...rest } = filters;
|
|
||||||
const res = await meta!.api.get<
|
|
||||||
AxiosResponse<QueryActionAuditLogResponse>
|
|
||||||
>(`/org/${orgId}/logs/action`, {
|
|
||||||
params: {
|
|
||||||
...rest,
|
|
||||||
limit: pageSize,
|
|
||||||
offset: page * pageSize
|
|
||||||
},
|
|
||||||
signal
|
|
||||||
});
|
|
||||||
return res.data.data;
|
|
||||||
},
|
|
||||||
refetchInterval: (query) => {
|
|
||||||
if (query.state.data) {
|
|
||||||
return durationToMs(30, "seconds");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
|
|
||||||
connection: ({
|
|
||||||
orgId,
|
|
||||||
filters
|
|
||||||
}: {
|
|
||||||
orgId: string;
|
|
||||||
filters: ConnectionLogFilters;
|
|
||||||
}) =>
|
|
||||||
queryOptions({
|
|
||||||
queryKey: ["CONNECTION_LOGS", orgId, "ALL", filters] as const,
|
|
||||||
queryFn: async ({ signal, meta }) => {
|
|
||||||
const { page, pageSize, ...rest } = filters;
|
|
||||||
const res = await meta!.api.get<
|
|
||||||
AxiosResponse<QueryConnectionAuditLogResponse>
|
|
||||||
>(`/org/${orgId}/logs/connection`, {
|
|
||||||
params: {
|
|
||||||
...rest,
|
|
||||||
limit: pageSize,
|
|
||||||
offset: page * pageSize
|
|
||||||
},
|
|
||||||
signal
|
|
||||||
});
|
|
||||||
return res.data.data;
|
|
||||||
},
|
|
||||||
refetchInterval: (query) => {
|
|
||||||
if (query.state.data) {
|
|
||||||
return durationToMs(30, "seconds");
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user