mirror of
https://github.com/Portabase/portabase.git
synced 2026-07-14 11:16:13 +02:00
feat: add http smoke tests.
This commit is contained in:
@@ -72,6 +72,7 @@ AUTH_PASSKEY_ENABLED=true
|
|||||||
|
|
||||||
# Retention
|
# Retention
|
||||||
RETENTION_CRON="* * * * *"
|
RETENTION_CRON="* * * * *"
|
||||||
|
AUDIT_LOG_RETENTION_DAYS=180
|
||||||
|
|
||||||
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056"
|
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056"
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -18,6 +18,7 @@ import * as backupStorage from "@/db/schema/14_storage-backup";
|
|||||||
import * as healthcheckLog from "@/db/schema/15_healthcheck-log";
|
import * as healthcheckLog from "@/db/schema/15_healthcheck-log";
|
||||||
import * as apiKey from "@/db/schema/16_apikey";
|
import * as apiKey from "@/db/schema/16_apikey";
|
||||||
import * as jobLog from "@/db/schema/17_job-log";
|
import * as jobLog from "@/db/schema/17_job-log";
|
||||||
|
import * as auditEvent from "@/db/schema/18_audit-event";
|
||||||
|
|
||||||
const log = logger.child({module: "db"});
|
const log = logger.child({module: "db"});
|
||||||
|
|
||||||
@@ -55,7 +56,8 @@ export const schemas = {
|
|||||||
...backupStorage,
|
...backupStorage,
|
||||||
...healthcheckLog,
|
...healthcheckLog,
|
||||||
...apiKey,
|
...apiKey,
|
||||||
...jobLog
|
...jobLog,
|
||||||
|
...auditEvent,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const db = drizzle({
|
export const db = drizzle({
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
CREATE TYPE "public"."audit_actor_type" AS ENUM('user', 'api_key', 'agent', 'system');--> statement-breakpoint
|
||||||
|
CREATE TYPE "public"."audit_outcome" AS ENUM('success', 'failure', 'denied');--> statement-breakpoint
|
||||||
|
CREATE TABLE "audit_events" (
|
||||||
|
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
||||||
|
"created_at" timestamp with time zone DEFAULT now() NOT NULL,
|
||||||
|
"event_type" varchar(128) NOT NULL,
|
||||||
|
"category" varchar(64) NOT NULL,
|
||||||
|
"outcome" "audit_outcome" NOT NULL,
|
||||||
|
"actor_type" "audit_actor_type" NOT NULL,
|
||||||
|
"actor_id" uuid,
|
||||||
|
"actor_name" text,
|
||||||
|
"actor_api_key_id" uuid,
|
||||||
|
"actor_api_key_name" text,
|
||||||
|
"organization_id" uuid,
|
||||||
|
"organization_name" text,
|
||||||
|
"target_type" varchar(64),
|
||||||
|
"target_id" uuid,
|
||||||
|
"target_name" text,
|
||||||
|
"ip_address" "inet",
|
||||||
|
"user_agent" text,
|
||||||
|
"metadata" jsonb DEFAULT '{}'::jsonb NOT NULL
|
||||||
|
);
|
||||||
|
--> statement-breakpoint
|
||||||
|
CREATE INDEX "audit_events_created_at_idx" ON "audit_events" USING btree ("created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "audit_events_organization_created_at_idx" ON "audit_events" USING btree ("organization_id","created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "audit_events_category_created_at_idx" ON "audit_events" USING btree ("category","created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "audit_events_event_type_created_at_idx" ON "audit_events" USING btree ("event_type","created_at");--> statement-breakpoint
|
||||||
|
CREATE INDEX "audit_events_outcome_created_at_idx" ON "audit_events" USING btree ("outcome","created_at");
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -435,6 +435,13 @@
|
|||||||
"when": 1780822414802,
|
"when": 1780822414802,
|
||||||
"tag": "0061_illegal_mole_man",
|
"tag": "0061_illegal_mole_man",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 62,
|
||||||
|
"version": "7",
|
||||||
|
"when": 1781507606925,
|
||||||
|
"tag": "0062_old_quasimodo",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import { sql } from "drizzle-orm";
|
||||||
|
import {
|
||||||
|
index,
|
||||||
|
inet,
|
||||||
|
jsonb,
|
||||||
|
pgEnum,
|
||||||
|
pgTable,
|
||||||
|
text,
|
||||||
|
timestamp,
|
||||||
|
uuid,
|
||||||
|
varchar,
|
||||||
|
} from "drizzle-orm/pg-core";
|
||||||
|
import { createSelectSchema } from "drizzle-zod";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
export const auditActorTypeEnum = pgEnum("audit_actor_type", [
|
||||||
|
"user",
|
||||||
|
"api_key",
|
||||||
|
"agent",
|
||||||
|
"system",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const auditOutcomeEnum = pgEnum("audit_outcome", [
|
||||||
|
"success",
|
||||||
|
"failure",
|
||||||
|
"denied",
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const auditEvent = pgTable(
|
||||||
|
"audit_events",
|
||||||
|
{
|
||||||
|
id: uuid("id").defaultRandom().primaryKey(),
|
||||||
|
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
|
||||||
|
eventType: varchar("event_type", { length: 128 }).notNull(),
|
||||||
|
category: varchar("category", { length: 64 }).notNull(),
|
||||||
|
outcome: auditOutcomeEnum("outcome").notNull(),
|
||||||
|
actorType: auditActorTypeEnum("actor_type").notNull(),
|
||||||
|
actorId: uuid("actor_id"),
|
||||||
|
actorName: text("actor_name"),
|
||||||
|
actorApiKeyId: uuid("actor_api_key_id"),
|
||||||
|
actorApiKeyName: text("actor_api_key_name"),
|
||||||
|
organizationId: uuid("organization_id"),
|
||||||
|
organizationName: text("organization_name"),
|
||||||
|
targetType: varchar("target_type", { length: 64 }),
|
||||||
|
targetId: uuid("target_id"),
|
||||||
|
targetName: text("target_name"),
|
||||||
|
ipAddress: inet("ip_address"),
|
||||||
|
userAgent: text("user_agent"),
|
||||||
|
metadata: jsonb("metadata")
|
||||||
|
.$type<Record<string, unknown>>()
|
||||||
|
.default(sql`'{}'::jsonb`)
|
||||||
|
.notNull(),
|
||||||
|
},
|
||||||
|
(table) => [
|
||||||
|
index("audit_events_created_at_idx").on(table.createdAt),
|
||||||
|
index("audit_events_organization_created_at_idx").on(table.organizationId, table.createdAt),
|
||||||
|
index("audit_events_category_created_at_idx").on(table.category, table.createdAt),
|
||||||
|
index("audit_events_event_type_created_at_idx").on(table.eventType, table.createdAt),
|
||||||
|
index("audit_events_outcome_created_at_idx").on(table.outcome, table.createdAt),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
|
export const auditEventSchema = createSelectSchema(auditEvent);
|
||||||
|
export type AuditEvent = z.infer<typeof auditEventSchema>;
|
||||||
+3
-1
@@ -52,6 +52,8 @@ export const env = createEnv({
|
|||||||
process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *",
|
process.env.NODE_ENV === "production" ? "0 7 * * *" : "* * * * *",
|
||||||
),
|
),
|
||||||
|
|
||||||
|
AUDIT_LOG_RETENTION_DAYS: z.coerce.number().int().default(180),
|
||||||
|
|
||||||
CLEANING_HEALTHCHECK_LOGS_CRON: z
|
CLEANING_HEALTHCHECK_LOGS_CRON: z
|
||||||
.string()
|
.string()
|
||||||
.default(
|
.default(
|
||||||
@@ -144,6 +146,7 @@ export const env = createEnv({
|
|||||||
SMTP_SECURE: process.env.SMTP_SECURE,
|
SMTP_SECURE: process.env.SMTP_SECURE,
|
||||||
|
|
||||||
RETENTION_CRON: process.env.RETENTION_CRON,
|
RETENTION_CRON: process.env.RETENTION_CRON,
|
||||||
|
AUDIT_LOG_RETENTION_DAYS: process.env.AUDIT_LOG_RETENTION_DAYS,
|
||||||
CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON,
|
CLEANING_HEALTHCHECK_LOGS_CRON: process.env.CLEANING_HEALTHCHECK_LOGS_CRON,
|
||||||
|
|
||||||
AUTH_OIDC_ID: process.env.AUTH_OIDC_ID,
|
AUTH_OIDC_ID: process.env.AUTH_OIDC_ID,
|
||||||
@@ -200,4 +203,3 @@ export const env = createEnv({
|
|||||||
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import {
|
||||||
|
auditEventEnrichmentSchema, type AuditEventEnrichment, type WithAuditEventConfigBase, withAuditEventConfigSchema,
|
||||||
|
} from "@/lib/audit/schema";
|
||||||
|
import {createAuditEvent} from "@/lib/audit/create-audit-event";
|
||||||
|
|
||||||
|
|
||||||
|
type WithAuditEventCallback<TResult> = (value: TResult | unknown) => AuditEventEnrichment | void;
|
||||||
|
type WithAuditEventResult = { success: boolean };
|
||||||
|
|
||||||
|
export type WithAuditEventConfig<TResult extends WithAuditEventResult> = WithAuditEventConfigBase & {
|
||||||
|
onSuccess?: (result: TResult) => AuditEventEnrichment | void;
|
||||||
|
onFailure?: WithAuditEventCallback<TResult>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function withAuditEvent<TResult extends WithAuditEventResult>(
|
||||||
|
effect: () => Promise<TResult>,
|
||||||
|
config: WithAuditEventConfig<TResult>,
|
||||||
|
) {
|
||||||
|
const {onSuccess, onFailure, ...rawConfig} = config;
|
||||||
|
const parsedConfig = withAuditEventConfigSchema.parse(rawConfig);
|
||||||
|
|
||||||
|
const writeOutcome = async (
|
||||||
|
outcome: "success" | "failure" | "denied",
|
||||||
|
enrichmentValue: AuditEventEnrichment | void,
|
||||||
|
) => {
|
||||||
|
const enrichment = enrichmentValue ? auditEventEnrichmentSchema.parse(enrichmentValue) : {};
|
||||||
|
|
||||||
|
await createAuditEvent({
|
||||||
|
eventType: parsedConfig.eventType,
|
||||||
|
outcome,
|
||||||
|
actor: parsedConfig.actor,
|
||||||
|
organization: enrichment?.organization === undefined ? parsedConfig.organization ?? null : enrichment.organization ?? null,
|
||||||
|
target: enrichment?.target === undefined ? parsedConfig.target ?? null : enrichment.target ?? null,
|
||||||
|
ipAddress: parsedConfig.ipAddress ?? null,
|
||||||
|
userAgent: parsedConfig.userAgent ?? null,
|
||||||
|
metadata: {...parsedConfig.metadata, ...enrichment.metadata},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = await effect();
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
await writeOutcome("failure", onFailure?.(result));
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeOutcome("success", onSuccess?.(result));
|
||||||
|
return result;
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
await writeOutcome("failure", onFailure?.(error));
|
||||||
|
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user