mirror of
https://github.com/fosrl/pangolin.git
synced 2026-01-28 22:00:51 +00:00
Restrict features
This commit is contained in:
@@ -1925,5 +1925,22 @@
|
||||
"sidebarLogsRequest": "Request Logs",
|
||||
"sidebarLogsAccess": "Access Logs",
|
||||
"sidebarLogsAction": "Action Logs",
|
||||
"requestLogsDescription": "View detailed pre-request logs for resources in this organization"
|
||||
}
|
||||
"logRetention": "Log Retention",
|
||||
"logRetentionDescription": "Manage how long different types of logs are retained for this organization or disable them",
|
||||
"requestLogsDescription": "View detailed request logs for resources in this organization",
|
||||
"logRetentionRequestLabel": "Request Log Retention",
|
||||
"logRetentionRequestDescription": "How long to retain request logs",
|
||||
"logRetentionAccessLabel": "Access Log Retention",
|
||||
"logRetentionAccessDescription": "How long to retain access logs",
|
||||
"logRetentionActionLabel": "Action Log Retention",
|
||||
"logRetentionActionDescription": "How long to retain action logs",
|
||||
"logRetentionDisabled": "Disabled",
|
||||
"logRetention3Days": "3 days",
|
||||
"logRetention7Days": "7 days",
|
||||
"logRetention14Days": "14 days",
|
||||
"logRetention30Days": "30 days",
|
||||
"logRetentionForever": "Forever",
|
||||
"actionLogsDescription": "View a history of actions performed in this organization",
|
||||
"accessLogsDescription": "View access auth requests for resources in this organization",
|
||||
"licenseRequiredToUse": "An Enterprise license is required to use this feature."
|
||||
}
|
||||
@@ -29,13 +29,13 @@ export const orgs = pgTable("orgs", {
|
||||
createdAt: text("createdAt"),
|
||||
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
|
||||
.notNull()
|
||||
.default(15),
|
||||
.default(7),
|
||||
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
|
||||
.notNull()
|
||||
.default(15),
|
||||
.default(0),
|
||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
|
||||
.notNull()
|
||||
.default(15),
|
||||
.default(0)
|
||||
});
|
||||
|
||||
export const orgDomains = pgTable("orgDomains", {
|
||||
|
||||
@@ -22,13 +22,13 @@ export const orgs = sqliteTable("orgs", {
|
||||
createdAt: text("createdAt"),
|
||||
settingsLogRetentionDaysRequest: integer("settingsLogRetentionDaysRequest") // where 0 = dont keep logs and -1 = keep forever
|
||||
.notNull()
|
||||
.default(15),
|
||||
.default(7),
|
||||
settingsLogRetentionDaysAccess: integer("settingsLogRetentionDaysAccess")
|
||||
.notNull()
|
||||
.default(15),
|
||||
.default(0),
|
||||
settingsLogRetentionDaysAction: integer("settingsLogRetentionDaysAction")
|
||||
.notNull()
|
||||
.default(15)
|
||||
.default(0)
|
||||
});
|
||||
|
||||
export const userDomains = sqliteTable("userDomains", {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { runSetupFunctions } from "./setup";
|
||||
import { createApiServer } from "./apiServer";
|
||||
import { createNextServer } from "./nextServer";
|
||||
import { createInternalServer } from "./internalServer";
|
||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||
import {
|
||||
ApiKey,
|
||||
ApiKeyOrg,
|
||||
@@ -13,13 +14,13 @@ import {
|
||||
User,
|
||||
UserOrg
|
||||
} from "@server/db";
|
||||
import { createIntegrationApiServer } from "./integrationApiServer";
|
||||
import config from "@server/lib/config";
|
||||
import { setHostMeta } from "@server/lib/hostMeta";
|
||||
import { initTelemetryClient } from "./lib/telemetry.js";
|
||||
import { TraefikConfigManager } from "./lib/traefik/TraefikConfigManager.js";
|
||||
import { initTelemetryClient } from "@server/lib/telemetry";
|
||||
import { TraefikConfigManager } from "@server/lib/traefik/TraefikConfigManager";
|
||||
import { initCleanup } from "#dynamic/cleanup";
|
||||
import license from "#dynamic/license/license";
|
||||
import { initLogCleanupInterval } from "@server/lib/cleanupLogs";
|
||||
|
||||
async function startServers() {
|
||||
await setHostMeta();
|
||||
@@ -33,6 +34,8 @@ async function startServers() {
|
||||
|
||||
initTelemetryClient();
|
||||
|
||||
initLogCleanupInterval();
|
||||
|
||||
// Start all servers
|
||||
const apiServer = createApiServer();
|
||||
const internalServer = createInternalServer();
|
||||
|
||||
62
server/lib/cleanupLogs.ts
Normal file
62
server/lib/cleanupLogs.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
import { db, orgs } from "@server/db";
|
||||
import { cleanUpOldLogs as cleanUpOldAccessLogs } from "@server/private/lib/logAccessAudit";
|
||||
import { cleanUpOldLogs as cleanUpOldActionLogs } from "@server/private/middlewares/logActionAudit";
|
||||
import { cleanUpOldLogs as cleanUpOldRequestLogs } from "@server/routers/badger/logRequestAudit";
|
||||
import { gt, or } from "drizzle-orm";
|
||||
|
||||
export function initLogCleanupInterval() {
|
||||
return setInterval(
|
||||
async () => {
|
||||
const orgsToClean = await db
|
||||
.select({
|
||||
orgId: orgs.orgId,
|
||||
settingsLogRetentionDaysAction:
|
||||
orgs.settingsLogRetentionDaysAction,
|
||||
settingsLogRetentionDaysAccess:
|
||||
orgs.settingsLogRetentionDaysAccess,
|
||||
settingsLogRetentionDaysRequest:
|
||||
orgs.settingsLogRetentionDaysRequest
|
||||
})
|
||||
.from(orgs)
|
||||
.where(
|
||||
or(
|
||||
gt(orgs.settingsLogRetentionDaysAction, 0),
|
||||
gt(orgs.settingsLogRetentionDaysAccess, 0),
|
||||
gt(orgs.settingsLogRetentionDaysRequest, 0)
|
||||
)
|
||||
);
|
||||
|
||||
for (const org of orgsToClean) {
|
||||
const {
|
||||
orgId,
|
||||
settingsLogRetentionDaysAction,
|
||||
settingsLogRetentionDaysAccess,
|
||||
settingsLogRetentionDaysRequest
|
||||
} = org;
|
||||
|
||||
if (settingsLogRetentionDaysAction > 0) {
|
||||
await cleanUpOldActionLogs(
|
||||
orgId,
|
||||
settingsLogRetentionDaysRequest
|
||||
);
|
||||
}
|
||||
|
||||
if (settingsLogRetentionDaysAccess > 0) {
|
||||
await cleanUpOldAccessLogs(
|
||||
orgId,
|
||||
settingsLogRetentionDaysRequest
|
||||
);
|
||||
}
|
||||
|
||||
if (settingsLogRetentionDaysRequest > 0) {
|
||||
await cleanUpOldRequestLogs(
|
||||
orgId,
|
||||
settingsLogRetentionDaysRequest
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
// 3 * 60 * 60 * 1000
|
||||
60 * 1000 // for testing
|
||||
); // every 3 hours
|
||||
}
|
||||
@@ -1,7 +1,8 @@
|
||||
import { accessAuditLog, db, orgs } from "@server/db";
|
||||
import { getCountryCodeForIp } from "@server/lib/geoip";
|
||||
import logger from "@server/logger";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
async function getAccessDays(orgId: string): Promise<number> {
|
||||
// check cache first
|
||||
@@ -23,11 +24,38 @@ async function getAccessDays(orgId: string): Promise<number> {
|
||||
}
|
||||
|
||||
// store the result in cache
|
||||
cache.set(`org_${orgId}_accessDays`, org.settingsLogRetentionDaysAction);
|
||||
cache.set(
|
||||
`org_${orgId}_accessDays`,
|
||||
org.settingsLogRetentionDaysAction,
|
||||
300
|
||||
);
|
||||
|
||||
return org.settingsLogRetentionDaysAction;
|
||||
}
|
||||
|
||||
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
|
||||
|
||||
try {
|
||||
const deleteResult = await db
|
||||
.delete(accessAuditLog)
|
||||
.where(
|
||||
and(
|
||||
lt(accessAuditLog.timestamp, cutoffTimestamp),
|
||||
eq(accessAuditLog.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Cleaned up ${deleteResult.changes} access audit logs older than ${retentionDays} days`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error cleaning up old action audit logs:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logAccessAudit(data: {
|
||||
action: boolean;
|
||||
type: string;
|
||||
@@ -40,6 +68,12 @@ export async function logAccessAudit(data: {
|
||||
requestIp?: string;
|
||||
}) {
|
||||
try {
|
||||
const retentionDays = await getAccessDays(data.orgId);
|
||||
if (retentionDays === 0) {
|
||||
// do not log
|
||||
return;
|
||||
}
|
||||
|
||||
let actorType: string | undefined;
|
||||
let actor: string | undefined;
|
||||
let actorId: string | undefined;
|
||||
|
||||
@@ -17,10 +17,9 @@ import logger from "@server/logger";
|
||||
import HttpCode from "@server/types/HttpCode";
|
||||
import { Request, Response, NextFunction } from "express";
|
||||
import createHttpError from "http-errors";
|
||||
import NodeCache from "node-cache";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes
|
||||
async function getActionDays(orgId: string): Promise<number> {
|
||||
// check cache first
|
||||
const cached = cache.get<number>(`org_${orgId}_actionDays`);
|
||||
@@ -41,11 +40,34 @@ async function getActionDays(orgId: string): Promise<number> {
|
||||
}
|
||||
|
||||
// store the result in cache
|
||||
cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction);
|
||||
cache.set(`org_${orgId}_actionDays`, org.settingsLogRetentionDaysAction, 300);
|
||||
|
||||
return org.settingsLogRetentionDaysAction;
|
||||
}
|
||||
|
||||
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
|
||||
|
||||
try {
|
||||
const deleteResult = await db
|
||||
.delete(actionAuditLog)
|
||||
.where(
|
||||
and(
|
||||
lt(actionAuditLog.timestamp, cutoffTimestamp),
|
||||
eq(actionAuditLog.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Cleaned up ${deleteResult.changes} action audit logs older than ${retentionDays} days`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error cleaning up old action audit logs:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export function logActionAudit(action: ActionsEnum) {
|
||||
return async function (
|
||||
req: Request,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { db, orgs, requestAuditLog } from "@server/db";
|
||||
import logger from "@server/logger";
|
||||
import { eq } from "drizzle-orm";
|
||||
import NodeCache from "node-cache";
|
||||
import { and, eq, lt } from "drizzle-orm";
|
||||
import cache from "@server/lib/cache";
|
||||
|
||||
/**
|
||||
|
||||
@@ -24,7 +24,6 @@ Reasons:
|
||||
|
||||
*/
|
||||
|
||||
const cache = new NodeCache({ stdTTL: 300 }); // cache for 5 minutes
|
||||
async function getRetentionDays(orgId: string): Promise<number> {
|
||||
// check cache first
|
||||
const cached = cache.get<number>(`org_${orgId}_retentionDays`);
|
||||
@@ -34,7 +33,8 @@ async function getRetentionDays(orgId: string): Promise<number> {
|
||||
|
||||
const [org] = await db
|
||||
.select({
|
||||
settingsLogRetentionDaysRequest: orgs.settingsLogRetentionDaysRequest
|
||||
settingsLogRetentionDaysRequest:
|
||||
orgs.settingsLogRetentionDaysRequest
|
||||
})
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
@@ -45,11 +45,38 @@ async function getRetentionDays(orgId: string): Promise<number> {
|
||||
}
|
||||
|
||||
// store the result in cache
|
||||
cache.set(`org_${orgId}_retentionDays`, org.settingsLogRetentionDaysRequest);
|
||||
cache.set(
|
||||
`org_${orgId}_retentionDays`,
|
||||
org.settingsLogRetentionDaysRequest,
|
||||
300
|
||||
);
|
||||
|
||||
return org.settingsLogRetentionDaysRequest;
|
||||
}
|
||||
|
||||
export async function cleanUpOldLogs(orgId: string, retentionDays: number) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
|
||||
const cutoffTimestamp = now - retentionDays * 24 * 60 * 60;
|
||||
|
||||
try {
|
||||
const deleteResult = await db
|
||||
.delete(requestAuditLog)
|
||||
.where(
|
||||
and(
|
||||
lt(requestAuditLog.timestamp, cutoffTimestamp),
|
||||
eq(requestAuditLog.orgId, orgId)
|
||||
)
|
||||
);
|
||||
|
||||
logger.info(
|
||||
`Cleaned up ${deleteResult.changes} request audit logs older than ${retentionDays} days`
|
||||
);
|
||||
} catch (error) {
|
||||
logger.error("Error cleaning up old request audit logs:", error);
|
||||
}
|
||||
}
|
||||
|
||||
export async function logRequestAudit(
|
||||
data: {
|
||||
action: boolean;
|
||||
@@ -76,7 +103,6 @@ export async function logRequestAudit(
|
||||
}
|
||||
) {
|
||||
try {
|
||||
|
||||
if (data.orgId) {
|
||||
const retentionDays = await getRetentionDays(data.orgId);
|
||||
if (retentionDays === 0) {
|
||||
|
||||
@@ -49,13 +49,13 @@ export async function getOrg(
|
||||
|
||||
const { orgId } = parsedParams.data;
|
||||
|
||||
const org = await db
|
||||
const [org] = await db
|
||||
.select()
|
||||
.from(orgs)
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.limit(1);
|
||||
|
||||
if (org.length === 0) {
|
||||
if (!org) {
|
||||
return next(
|
||||
createHttpError(
|
||||
HttpCode.NOT_FOUND,
|
||||
@@ -66,7 +66,7 @@ export async function getOrg(
|
||||
|
||||
return response<GetOrgResponse>(res, {
|
||||
data: {
|
||||
org: org[0]
|
||||
org
|
||||
},
|
||||
success: true,
|
||||
error: false,
|
||||
|
||||
@@ -18,7 +18,10 @@ const updateOrgParamsSchema = z
|
||||
|
||||
const updateOrgBodySchema = z
|
||||
.object({
|
||||
name: z.string().min(1).max(255).optional()
|
||||
name: z.string().min(1).max(255).optional(),
|
||||
settingsLogRetentionDaysRequest: z.number().min(-1).optional(),
|
||||
settingsLogRetentionDaysAccess: z.number().min(-1).optional(),
|
||||
settingsLogRetentionDaysAction: z.number().min(-1).optional()
|
||||
})
|
||||
.strict()
|
||||
.refine((data) => Object.keys(data).length > 0, {
|
||||
@@ -74,7 +77,7 @@ export async function updateOrg(
|
||||
const updatedOrg = await db
|
||||
.update(orgs)
|
||||
.set({
|
||||
name: parsedBody.data.name
|
||||
...parsedBody.data
|
||||
})
|
||||
.where(eq(orgs.orgId, orgId))
|
||||
.returning();
|
||||
|
||||
@@ -42,15 +42,36 @@ import {
|
||||
import { useUserContext } from "@app/hooks/useUserContext";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { build } from "@server/build";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger
|
||||
} from "@app/components/ui/dropdown-menu";
|
||||
import { ChevronDown } from "lucide-react";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
|
||||
// Schema for general organization settings
|
||||
const GeneralFormSchema = z.object({
|
||||
name: z.string(),
|
||||
subnet: z.string().optional()
|
||||
subnet: z.string().optional(),
|
||||
settingsLogRetentionDaysRequest: z.number(),
|
||||
settingsLogRetentionDaysAccess: z.number(),
|
||||
settingsLogRetentionDaysAction: z.number()
|
||||
});
|
||||
|
||||
type GeneralFormValues = z.infer<typeof GeneralFormSchema>;
|
||||
|
||||
const LOG_RETENTION_OPTIONS = [
|
||||
{ label: "logRetentionDisabled", value: 0 },
|
||||
{ label: "logRetention3Days", value: 3 },
|
||||
{ label: "logRetention7Days", value: 7 },
|
||||
{ label: "logRetention14Days", value: 14 },
|
||||
{ label: "logRetention30Days", value: 30 },
|
||||
{ label: "logRetentionForever", value: -1 }
|
||||
];
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
const { orgUser } = userOrgUserContext();
|
||||
@@ -60,6 +81,8 @@ export default function GeneralPage() {
|
||||
const { user } = useUserContext();
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
|
||||
const [loadingDelete, setLoadingDelete] = useState(false);
|
||||
const [loadingSave, setLoadingSave] = useState(false);
|
||||
@@ -69,7 +92,13 @@ export default function GeneralPage() {
|
||||
resolver: zodResolver(GeneralFormSchema),
|
||||
defaultValues: {
|
||||
name: org?.org.name,
|
||||
subnet: org?.org.subnet || "" // Add default value for subnet
|
||||
subnet: org?.org.subnet || "", // Add default value for subnet
|
||||
settingsLogRetentionDaysRequest:
|
||||
org.org.settingsLogRetentionDaysRequest ?? 15,
|
||||
settingsLogRetentionDaysAccess:
|
||||
org.org.settingsLogRetentionDaysAccess ?? 15,
|
||||
settingsLogRetentionDaysAction:
|
||||
org.org.settingsLogRetentionDaysAction ?? 15
|
||||
},
|
||||
mode: "onChange"
|
||||
});
|
||||
@@ -131,8 +160,14 @@ export default function GeneralPage() {
|
||||
try {
|
||||
// Update organization
|
||||
await api.post(`/org/${org?.org.orgId}`, {
|
||||
name: data.name
|
||||
name: data.name,
|
||||
// subnet: data.subnet // Include subnet in the API request
|
||||
settingsLogRetentionDaysRequest:
|
||||
data.settingsLogRetentionDaysRequest,
|
||||
settingsLogRetentionDaysAccess:
|
||||
data.settingsLogRetentionDaysAccess,
|
||||
settingsLogRetentionDaysAction:
|
||||
data.settingsLogRetentionDaysAction
|
||||
});
|
||||
|
||||
// Also save auth page settings if they have unsaved changes
|
||||
@@ -159,6 +194,11 @@ export default function GeneralPage() {
|
||||
}
|
||||
}
|
||||
|
||||
const getLabelForValue = (value: number) => {
|
||||
const option = LOG_RETENTION_OPTIONS.find((opt) => opt.value === value);
|
||||
return option ? t(option.label) : `${value} days`;
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingsContainer>
|
||||
<ConfirmDeleteDialog
|
||||
@@ -168,9 +208,7 @@ export default function GeneralPage() {
|
||||
}}
|
||||
dialog={
|
||||
<div>
|
||||
<p>
|
||||
{t("orgQuestionRemove")}
|
||||
</p>
|
||||
<p>{t("orgQuestionRemove")}</p>
|
||||
<p>{t("orgMessageRemove")}</p>
|
||||
</div>
|
||||
}
|
||||
@@ -179,23 +217,24 @@ export default function GeneralPage() {
|
||||
string={org?.org.name || ""}
|
||||
title={t("orgDelete")}
|
||||
/>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("orgGeneralSettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("orgGeneralSettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="org-settings-form"
|
||||
>
|
||||
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(onSubmit)}
|
||||
className="space-y-4"
|
||||
id="org-settings-form"
|
||||
>
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("orgGeneralSettings")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("orgGeneralSettingsDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
<SettingsSectionForm>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
@@ -235,15 +274,228 @@ export default function GeneralPage() {
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
</form>
|
||||
</Form>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
|
||||
{(build === "saas") && (
|
||||
<AuthPageSettings ref={authPageSettingsRef} />
|
||||
)}
|
||||
<SettingsSection>
|
||||
<SettingsSectionHeader>
|
||||
<SettingsSectionTitle>
|
||||
{t("logRetention")}
|
||||
</SettingsSectionTitle>
|
||||
<SettingsSectionDescription>
|
||||
{t("logRetentionDescription")}
|
||||
</SettingsSectionDescription>
|
||||
</SettingsSectionHeader>
|
||||
<SettingsSectionBody>
|
||||
{/* {build === "saas" && !subscription?.subscribed ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("orgAuthPageDisabled")}{" "}
|
||||
{t("subscriptionRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null} */}
|
||||
|
||||
<SettingsSectionForm>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="settingsLogRetentionDaysRequest"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t("logRetentionRequestLabel")}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-between"
|
||||
>
|
||||
{getLabelForValue(
|
||||
field.value
|
||||
)}
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{LOG_RETENTION_OPTIONS.map(
|
||||
(option) => (
|
||||
<DropdownMenuItem
|
||||
key={
|
||||
option.value
|
||||
}
|
||||
onClick={() =>
|
||||
field.onChange(
|
||||
option.value
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(
|
||||
option.label
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"logRetentionRequestDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
{build != "oss" && (
|
||||
<>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="settingsLogRetentionDaysAccess"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"logRetentionAccessLabel"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-between"
|
||||
disabled={
|
||||
(build ==
|
||||
"saas" &&
|
||||
!subscription?.subscribed) ||
|
||||
(build ==
|
||||
"enterprise" &&
|
||||
!isUnlocked())
|
||||
}
|
||||
>
|
||||
{getLabelForValue(
|
||||
field.value
|
||||
)}
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{LOG_RETENTION_OPTIONS.map(
|
||||
(
|
||||
option
|
||||
) => (
|
||||
<DropdownMenuItem
|
||||
key={
|
||||
option.value
|
||||
}
|
||||
onClick={() =>
|
||||
field.onChange(
|
||||
option.value
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(
|
||||
option.label
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"logRetentionAccessDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="settingsLogRetentionDaysAction"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t(
|
||||
"logRetentionActionLabel"
|
||||
)}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger
|
||||
asChild
|
||||
>
|
||||
<Button
|
||||
variant="outline"
|
||||
className="w-full justify-between"
|
||||
disabled={
|
||||
(build ==
|
||||
"saas" &&
|
||||
!subscription?.subscribed) ||
|
||||
(build ==
|
||||
"enterprise" &&
|
||||
!isUnlocked())
|
||||
}
|
||||
>
|
||||
{getLabelForValue(
|
||||
field.value
|
||||
)}
|
||||
<ChevronDown className="h-4 w-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-full">
|
||||
{LOG_RETENTION_OPTIONS.map(
|
||||
(
|
||||
option
|
||||
) => (
|
||||
<DropdownMenuItem
|
||||
key={
|
||||
option.value
|
||||
}
|
||||
onClick={() =>
|
||||
field.onChange(
|
||||
option.value
|
||||
)
|
||||
}
|
||||
>
|
||||
{t(
|
||||
option.label
|
||||
)}
|
||||
</DropdownMenuItem>
|
||||
)
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</FormControl>
|
||||
<FormDescription>
|
||||
{t(
|
||||
"logRetentionActionDescription"
|
||||
)}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</SettingsSectionForm>
|
||||
</SettingsSectionBody>
|
||||
</SettingsSection>
|
||||
</form>
|
||||
</Form>
|
||||
|
||||
{build === "saas" && <AuthPageSettings ref={authPageSettingsRef} />}
|
||||
|
||||
{/* Save Button */}
|
||||
<div className="flex justify-end">
|
||||
|
||||
@@ -6,12 +6,21 @@ import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable";
|
||||
import {
|
||||
getStoredPageSize,
|
||||
LogDataTable,
|
||||
setStoredPageSize
|
||||
} from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { ArrowUpRight, Key, User } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
@@ -21,6 +30,8 @@ export default function GeneralPage() {
|
||||
const t = useTranslations();
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -202,6 +213,16 @@ export default function GeneralPage() {
|
||||
}
|
||||
) => {
|
||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||
if (
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked())
|
||||
) {
|
||||
console.log(
|
||||
"Access denied: subscription inactive or license locked"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
@@ -583,6 +604,27 @@ export default function GeneralPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t("accessLogs")}
|
||||
description={t("accessLogsDescription")}
|
||||
/>
|
||||
|
||||
{build == "saas" && !subscription?.subscribed ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("subscriptionRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{build == "enterprise" && !isUnlocked() ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("licenseRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<LogDataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
@@ -610,6 +652,10 @@ export default function GeneralPage() {
|
||||
// Row expansion props
|
||||
expandable={true}
|
||||
renderExpandedRow={renderExpandedRow}
|
||||
disabled={
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked())
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -6,11 +6,20 @@ import { createApiClient } from "@app/lib/api";
|
||||
import { useEnvContext } from "@app/hooks/useEnvContext";
|
||||
import { useParams, useRouter, useSearchParams } from "next/navigation";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { getStoredPageSize, LogDataTable, setStoredPageSize } from "@app/components/LogDataTable";
|
||||
import {
|
||||
getStoredPageSize,
|
||||
LogDataTable,
|
||||
setStoredPageSize
|
||||
} from "@app/components/LogDataTable";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { DateTimeValue } from "@app/components/DateTimePicker";
|
||||
import { Key, User } from "lucide-react";
|
||||
import { ColumnFilter } from "@app/components/ColumnFilter";
|
||||
import SettingsSectionTitle from "@app/components/SettingsSectionTitle";
|
||||
import { useSubscriptionStatusContext } from "@app/hooks/useSubscriptionStatusContext";
|
||||
import { useLicenseStatusContext } from "@app/hooks/useLicenseStatusContext";
|
||||
import { build } from "@server/build";
|
||||
import { Alert, AlertDescription } from "@app/components/ui/alert";
|
||||
|
||||
export default function GeneralPage() {
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
@@ -20,6 +29,8 @@ export default function GeneralPage() {
|
||||
const { env } = useEnvContext();
|
||||
const { orgId } = useParams();
|
||||
const searchParams = useSearchParams();
|
||||
const subscription = useSubscriptionStatusContext();
|
||||
const { isUnlocked } = useLicenseStatusContext();
|
||||
|
||||
const [rows, setRows] = useState<any[]>([]);
|
||||
const [isRefreshing, setIsRefreshing] = useState(false);
|
||||
@@ -187,6 +198,15 @@ export default function GeneralPage() {
|
||||
}
|
||||
) => {
|
||||
console.log("Date range changed:", { startDate, endDate, page, size });
|
||||
if (
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked())
|
||||
) {
|
||||
console.log(
|
||||
"Access denied: subscription inactive or license locked"
|
||||
);
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
@@ -435,6 +455,27 @@ export default function GeneralPage() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsSectionTitle
|
||||
title={t("actionLogs")}
|
||||
description={t("actionLogsDescription")}
|
||||
/>
|
||||
|
||||
{build == "saas" && !subscription?.subscribed ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("subscriptionRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
{build == "enterprise" && !isUnlocked() ? (
|
||||
<Alert variant="info" className="mb-6">
|
||||
<AlertDescription>
|
||||
{t("licenseRequiredToUse")}
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
) : null}
|
||||
|
||||
<LogDataTable
|
||||
columns={columns}
|
||||
data={rows}
|
||||
@@ -464,6 +505,10 @@ export default function GeneralPage() {
|
||||
// Row expansion props
|
||||
expandable={true}
|
||||
renderExpandedRow={renderExpandedRow}
|
||||
disabled={
|
||||
(build == "saas" && !subscription?.subscribed) ||
|
||||
(build == "enterprise" && !isUnlocked())
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -123,16 +123,20 @@ export const orgNavSections = (
|
||||
href: "/{orgId}/settings/logs/request",
|
||||
icon: <SquareMousePointer className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "sidebarLogsAccess",
|
||||
href: "/{orgId}/settings/logs/access",
|
||||
icon: <ScanEye className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "sidebarLogsAction",
|
||||
href: "/{orgId}/settings/logs/action",
|
||||
icon: <Logs className="h-4 w-4" />
|
||||
},
|
||||
...(build != "oss"
|
||||
? [
|
||||
{
|
||||
title: "sidebarLogsAccess",
|
||||
href: "/{orgId}/settings/logs/access",
|
||||
icon: <ScanEye className="h-4 w-4" />
|
||||
},
|
||||
{
|
||||
title: "sidebarLogsAction",
|
||||
href: "/{orgId}/settings/logs/action",
|
||||
icon: <Logs className="h-4 w-4" />
|
||||
}
|
||||
]
|
||||
: [])
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
@@ -23,6 +23,7 @@ interface DataTablePaginationProps<TData> {
|
||||
totalCount?: number;
|
||||
isServerPagination?: boolean;
|
||||
isLoading?: boolean;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({
|
||||
@@ -31,7 +32,8 @@ export function DataTablePagination<TData>({
|
||||
onPageChange,
|
||||
totalCount,
|
||||
isServerPagination = false,
|
||||
isLoading = false
|
||||
isLoading = false,
|
||||
disabled = false
|
||||
}: DataTablePaginationProps<TData>) {
|
||||
const t = useTranslations();
|
||||
|
||||
@@ -96,8 +98,9 @@ export function DataTablePagination<TData>({
|
||||
<Select
|
||||
value={`${table.getState().pagination.pageSize}`}
|
||||
onValueChange={handlePageSizeChange}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="h-8 w-[73px]">
|
||||
<SelectTrigger className="h-8 w-[73px]" disabled={disabled}>
|
||||
<SelectValue
|
||||
placeholder={table.getState().pagination.pageSize}
|
||||
/>
|
||||
@@ -128,7 +131,7 @@ export function DataTablePagination<TData>({
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => handlePageNavigation('first')}
|
||||
disabled={!table.getCanPreviousPage() || isLoading}
|
||||
disabled={!table.getCanPreviousPage() || isLoading || disabled}
|
||||
>
|
||||
<span className="sr-only">{t('paginatorToFirst')}</span>
|
||||
<DoubleArrowLeftIcon className="h-4 w-4" />
|
||||
@@ -137,7 +140,7 @@ export function DataTablePagination<TData>({
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handlePageNavigation('previous')}
|
||||
disabled={!table.getCanPreviousPage() || isLoading}
|
||||
disabled={!table.getCanPreviousPage() || isLoading || disabled}
|
||||
>
|
||||
<span className="sr-only">{t('paginatorToPrevious')}</span>
|
||||
<ChevronLeftIcon className="h-4 w-4" />
|
||||
@@ -146,7 +149,7 @@ export function DataTablePagination<TData>({
|
||||
variant="outline"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => handlePageNavigation('next')}
|
||||
disabled={!table.getCanNextPage() || isLoading}
|
||||
disabled={!table.getCanNextPage() || isLoading || disabled}
|
||||
>
|
||||
<span className="sr-only">{t('paginatorToNext')}</span>
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
@@ -155,7 +158,7 @@ export function DataTablePagination<TData>({
|
||||
variant="outline"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => handlePageNavigation('last')}
|
||||
disabled={!table.getCanNextPage() || isLoading}
|
||||
disabled={!table.getCanNextPage() || isLoading || disabled}
|
||||
>
|
||||
<span className="sr-only">{t('paginatorToLast')}</span>
|
||||
<DoubleArrowRightIcon className="h-4 w-4" />
|
||||
|
||||
@@ -102,6 +102,7 @@ type DataTableProps<TData, TValue> = {
|
||||
};
|
||||
tabs?: TabFilter[];
|
||||
defaultTab?: string;
|
||||
disabled?: boolean;
|
||||
onDateRangeChange?: (
|
||||
startDate: DateTimeValue,
|
||||
endDate: DateTimeValue
|
||||
@@ -144,6 +145,7 @@ export function LogDataTable<TData, TValue>({
|
||||
onPageSizeChange: onPageSizeChangeProp,
|
||||
isLoading = false,
|
||||
expandable = false,
|
||||
disabled=false,
|
||||
renderExpandedRow
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const t = useTranslations();
|
||||
@@ -175,6 +177,11 @@ export function LogDataTable<TData, TValue>({
|
||||
|
||||
// Apply tab filter to data
|
||||
const filteredData = useMemo(() => {
|
||||
// If disabled, return empty array to prevent data loading
|
||||
if (disabled) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!tabs || activeTab === "") {
|
||||
return data;
|
||||
}
|
||||
@@ -185,7 +192,7 @@ export function LogDataTable<TData, TValue>({
|
||||
}
|
||||
|
||||
return data.filter(activeTabFilter.filterFn);
|
||||
}, [data, tabs, activeTab]);
|
||||
}, [data, tabs, activeTab, disabled]);
|
||||
|
||||
// Toggle row expansion
|
||||
const toggleRowExpansion = (rowId: string) => {
|
||||
@@ -219,9 +226,12 @@ export function LogDataTable<TData, TValue>({
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="h-6 w-6 p-0"
|
||||
disabled={disabled}
|
||||
onClick={(e) => {
|
||||
toggleRowExpansion(row.id);
|
||||
e.stopPropagation();
|
||||
if (!disabled) {
|
||||
toggleRowExpansion(row.id);
|
||||
e.stopPropagation();
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isExpanded ? (
|
||||
@@ -236,7 +246,7 @@ export function LogDataTable<TData, TValue>({
|
||||
};
|
||||
|
||||
return [expansionColumn, ...columns];
|
||||
}, [columns, expandable, expandedRows, toggleRowExpansion]);
|
||||
}, [columns, expandable, expandedRows, toggleRowExpansion, disabled]);
|
||||
|
||||
const table = useReactTable({
|
||||
data: filteredData,
|
||||
@@ -298,6 +308,8 @@ export function LogDataTable<TData, TValue>({
|
||||
}, [currentPage, table, isServerPagination]);
|
||||
|
||||
const handleTabChange = (value: string) => {
|
||||
if (disabled) return;
|
||||
|
||||
setActiveTab(value);
|
||||
// Reset to first page when changing tabs
|
||||
table.setPageIndex(0);
|
||||
@@ -305,6 +317,8 @@ export function LogDataTable<TData, TValue>({
|
||||
|
||||
// Enhanced pagination component that updates our local state
|
||||
const handlePageSizeChange = (newPageSize: number) => {
|
||||
if (disabled) return;
|
||||
|
||||
// setPageSize(newPageSize);
|
||||
table.setPageSize(newPageSize);
|
||||
|
||||
@@ -321,6 +335,8 @@ export function LogDataTable<TData, TValue>({
|
||||
|
||||
// Handle page changes for server pagination
|
||||
const handlePageChange = (newPageIndex: number) => {
|
||||
if (disabled) return;
|
||||
|
||||
if (isServerPagination && onPageChange) {
|
||||
onPageChange(newPageIndex);
|
||||
}
|
||||
@@ -330,6 +346,8 @@ export function LogDataTable<TData, TValue>({
|
||||
start: DateTimeValue,
|
||||
end: DateTimeValue
|
||||
) => {
|
||||
if (disabled) return;
|
||||
|
||||
setStartDate(start);
|
||||
setEndDate(end);
|
||||
onDateRangeChange?.(start, end);
|
||||
@@ -358,14 +376,15 @@ export function LogDataTable<TData, TValue>({
|
||||
endValue={endDate}
|
||||
onRangeChange={handleDateRangeChange}
|
||||
className="flex-wrap gap-2"
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex items-start gap-2 sm:justify-end">
|
||||
{onRefresh && (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onRefresh}
|
||||
disabled={isRefreshing}
|
||||
onClick={() => !disabled && onRefresh()}
|
||||
disabled={isRefreshing || disabled}
|
||||
>
|
||||
<RefreshCw
|
||||
className={`mr-2 h-4 w-4 ${isRefreshing ? "animate-spin" : ""}`}
|
||||
@@ -374,7 +393,7 @@ export function LogDataTable<TData, TValue>({
|
||||
</Button>
|
||||
)}
|
||||
{onExport && (
|
||||
<Button onClick={onExport} disabled={isExporting}>
|
||||
<Button onClick={() => !disabled && onExport()} disabled={isExporting || disabled}>
|
||||
<Download
|
||||
className={`mr-2 h-4 w-4 ${isExporting ? "animate-spin" : ""}`}
|
||||
/>
|
||||
@@ -415,7 +434,7 @@ export function LogDataTable<TData, TValue>({
|
||||
"selected"
|
||||
}
|
||||
onClick={() =>
|
||||
expandable
|
||||
expandable && !disabled
|
||||
? toggleRowExpansion(
|
||||
row.id
|
||||
)
|
||||
@@ -500,6 +519,7 @@ export function LogDataTable<TData, TValue>({
|
||||
totalCount={totalCount}
|
||||
isServerPagination={isServerPagination}
|
||||
isLoading={isLoading}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</div>
|
||||
</CardContent>
|
||||
|
||||
Reference in New Issue
Block a user