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_CRON="* * * * *"
|
||||
AUDIT_LOG_RETENTION_DAYS=180
|
||||
|
||||
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 apiKey from "@/db/schema/16_apikey";
|
||||
import * as jobLog from "@/db/schema/17_job-log";
|
||||
import * as auditEvent from "@/db/schema/18_audit-event";
|
||||
|
||||
const log = logger.child({module: "db"});
|
||||
|
||||
@@ -55,7 +56,8 @@ export const schemas = {
|
||||
...backupStorage,
|
||||
...healthcheckLog,
|
||||
...apiKey,
|
||||
...jobLog
|
||||
...jobLog,
|
||||
...auditEvent,
|
||||
};
|
||||
|
||||
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,
|
||||
"tag": "0061_illegal_mole_man",
|
||||
"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 * * *" : "* * * * *",
|
||||
),
|
||||
|
||||
AUDIT_LOG_RETENTION_DAYS: z.coerce.number().int().default(180),
|
||||
|
||||
CLEANING_HEALTHCHECK_LOGS_CRON: z
|
||||
.string()
|
||||
.default(
|
||||
@@ -144,6 +146,7 @@ export const env = createEnv({
|
||||
SMTP_SECURE: process.env.SMTP_SECURE,
|
||||
|
||||
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,
|
||||
|
||||
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