convex-audit-log 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +408 -0
- package/dist/client/_generated/_ignore.d.ts +1 -0
- package/dist/client/_generated/_ignore.d.ts.map +1 -0
- package/dist/client/_generated/_ignore.js +3 -0
- package/dist/client/_generated/_ignore.js.map +1 -0
- package/dist/client/index.d.ts +336 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +297 -0
- package/dist/client/index.js.map +1 -0
- package/dist/component/_generated/api.d.ts +36 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +317 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +3 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/lib.d.ts +341 -0
- package/dist/component/lib.d.ts.map +1 -0
- package/dist/component/lib.js +598 -0
- package/dist/component/lib.js.map +1 -0
- package/dist/component/schema.d.ts +87 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +71 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/shared.d.ts +203 -0
- package/dist/component/shared.d.ts.map +1 -0
- package/dist/component/shared.js +94 -0
- package/dist/component/shared.js.map +1 -0
- package/dist/react/index.d.ts +247 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +196 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +115 -0
- package/src/client/_generated/_ignore.ts +1 -0
- package/src/client/index.test.ts +61 -0
- package/src/client/index.ts +525 -0
- package/src/client/setup.test.ts +26 -0
- package/src/component/_generated/api.ts +52 -0
- package/src/component/_generated/component.ts +392 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +161 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/lib.test.ts +171 -0
- package/src/component/lib.ts +722 -0
- package/src/component/schema.ts +93 -0
- package/src/component/setup.test.ts +11 -0
- package/src/component/shared.ts +167 -0
- package/src/react/index.ts +305 -0
- package/src/test.ts +18 -0
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Severity levels for audit events.
|
|
6
|
+
* - info: Regular operations (login, profile update)
|
|
7
|
+
* - warning: Potentially concerning actions (password change, permission change)
|
|
8
|
+
* - error: Failed operations or errors
|
|
9
|
+
* - critical: Security-sensitive events (unauthorized access attempts, data breaches)
|
|
10
|
+
*/
|
|
11
|
+
export const vSeverity = v.union(
|
|
12
|
+
v.literal("info"),
|
|
13
|
+
v.literal("warning"),
|
|
14
|
+
v.literal("error"),
|
|
15
|
+
v.literal("critical")
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
export type Severity = "info" | "warning" | "error" | "critical";
|
|
19
|
+
|
|
20
|
+
export default defineSchema({
|
|
21
|
+
/**
|
|
22
|
+
* Main audit log table storing all audit events.
|
|
23
|
+
*/
|
|
24
|
+
auditLogs: defineTable({
|
|
25
|
+
// Core event data
|
|
26
|
+
action: v.string(),
|
|
27
|
+
actorId: v.optional(v.string()),
|
|
28
|
+
timestamp: v.number(),
|
|
29
|
+
|
|
30
|
+
// Resource tracking
|
|
31
|
+
resourceType: v.optional(v.string()),
|
|
32
|
+
resourceId: v.optional(v.string()),
|
|
33
|
+
|
|
34
|
+
// Event metadata
|
|
35
|
+
metadata: v.optional(v.any()),
|
|
36
|
+
severity: vSeverity,
|
|
37
|
+
|
|
38
|
+
// Context information
|
|
39
|
+
ipAddress: v.optional(v.string()),
|
|
40
|
+
userAgent: v.optional(v.string()),
|
|
41
|
+
sessionId: v.optional(v.string()),
|
|
42
|
+
|
|
43
|
+
// Compliance tags
|
|
44
|
+
tags: v.optional(v.array(v.string())),
|
|
45
|
+
|
|
46
|
+
// Change tracking
|
|
47
|
+
before: v.optional(v.any()),
|
|
48
|
+
after: v.optional(v.any()),
|
|
49
|
+
diff: v.optional(v.string()),
|
|
50
|
+
|
|
51
|
+
// Retention category for cleanup policies
|
|
52
|
+
retentionCategory: v.optional(v.string()),
|
|
53
|
+
})
|
|
54
|
+
// Query by action type with time ordering
|
|
55
|
+
.index("by_action_timestamp", ["action", "timestamp"])
|
|
56
|
+
// Query by actor (user) with time ordering
|
|
57
|
+
.index("by_actor_timestamp", ["actorId", "timestamp"])
|
|
58
|
+
// Query by resource with time ordering
|
|
59
|
+
.index("by_resource", ["resourceType", "resourceId", "timestamp"])
|
|
60
|
+
// Query by severity level with time ordering
|
|
61
|
+
.index("by_severity_timestamp", ["severity", "timestamp"])
|
|
62
|
+
// Query by timestamp for time-range queries
|
|
63
|
+
.index("by_timestamp", ["timestamp"])
|
|
64
|
+
// Query by retention category for cleanup
|
|
65
|
+
.index("by_retention_timestamp", ["retentionCategory", "timestamp"]),
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Configuration table for component settings.
|
|
69
|
+
* Contains a single document with global configuration.
|
|
70
|
+
*/
|
|
71
|
+
config: defineTable({
|
|
72
|
+
// Retention settings
|
|
73
|
+
defaultRetentionDays: v.number(),
|
|
74
|
+
criticalRetentionDays: v.number(),
|
|
75
|
+
|
|
76
|
+
// PII redaction settings
|
|
77
|
+
piiFieldsToRedact: v.array(v.string()),
|
|
78
|
+
|
|
79
|
+
// Sampling settings (for high-volume apps)
|
|
80
|
+
samplingEnabled: v.boolean(),
|
|
81
|
+
samplingRate: v.number(), // 0.0 to 1.0
|
|
82
|
+
|
|
83
|
+
// Custom retention categories
|
|
84
|
+
customRetention: v.optional(
|
|
85
|
+
v.array(
|
|
86
|
+
v.object({
|
|
87
|
+
category: v.string(),
|
|
88
|
+
retentionDays: v.number(),
|
|
89
|
+
})
|
|
90
|
+
)
|
|
91
|
+
),
|
|
92
|
+
}),
|
|
93
|
+
});
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import { test } from "vitest";
|
|
3
|
+
import schema from "./schema.js";
|
|
4
|
+
import { convexTest } from "convex-test";
|
|
5
|
+
export const modules = import.meta.glob("./**/*.*s");
|
|
6
|
+
|
|
7
|
+
export function initConvexTest() {
|
|
8
|
+
const t = convexTest(schema, modules);
|
|
9
|
+
return t;
|
|
10
|
+
}
|
|
11
|
+
test("setup", () => {});
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import type { Infer } from "convex/values";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Severity levels for audit events.
|
|
6
|
+
*/
|
|
7
|
+
export const vSeverity = v.union(
|
|
8
|
+
v.literal("info"),
|
|
9
|
+
v.literal("warning"),
|
|
10
|
+
v.literal("error"),
|
|
11
|
+
v.literal("critical")
|
|
12
|
+
);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Base audit event validator (without timestamp - added automatically).
|
|
16
|
+
*/
|
|
17
|
+
export const vAuditEventInput = v.object({
|
|
18
|
+
action: v.string(),
|
|
19
|
+
actorId: v.optional(v.string()),
|
|
20
|
+
resourceType: v.optional(v.string()),
|
|
21
|
+
resourceId: v.optional(v.string()),
|
|
22
|
+
metadata: v.optional(v.any()),
|
|
23
|
+
severity: vSeverity,
|
|
24
|
+
ipAddress: v.optional(v.string()),
|
|
25
|
+
userAgent: v.optional(v.string()),
|
|
26
|
+
sessionId: v.optional(v.string()),
|
|
27
|
+
tags: v.optional(v.array(v.string())),
|
|
28
|
+
retentionCategory: v.optional(v.string()),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Change tracking event validator.
|
|
33
|
+
*/
|
|
34
|
+
export const vChangeEventInput = v.object({
|
|
35
|
+
action: v.string(),
|
|
36
|
+
actorId: v.optional(v.string()),
|
|
37
|
+
resourceType: v.string(),
|
|
38
|
+
resourceId: v.string(),
|
|
39
|
+
before: v.optional(v.any()),
|
|
40
|
+
after: v.optional(v.any()),
|
|
41
|
+
generateDiff: v.optional(v.boolean()),
|
|
42
|
+
severity: v.optional(vSeverity),
|
|
43
|
+
ipAddress: v.optional(v.string()),
|
|
44
|
+
userAgent: v.optional(v.string()),
|
|
45
|
+
sessionId: v.optional(v.string()),
|
|
46
|
+
tags: v.optional(v.array(v.string())),
|
|
47
|
+
retentionCategory: v.optional(v.string()),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Query filters validator.
|
|
52
|
+
*/
|
|
53
|
+
export const vQueryFilters = v.object({
|
|
54
|
+
severity: v.optional(v.array(vSeverity)),
|
|
55
|
+
actions: v.optional(v.array(v.string())),
|
|
56
|
+
resourceTypes: v.optional(v.array(v.string())),
|
|
57
|
+
actorIds: v.optional(v.array(v.string())),
|
|
58
|
+
fromTimestamp: v.optional(v.number()),
|
|
59
|
+
toTimestamp: v.optional(v.number()),
|
|
60
|
+
tags: v.optional(v.array(v.string())),
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Pagination validator.
|
|
65
|
+
*/
|
|
66
|
+
export const vPagination = v.object({
|
|
67
|
+
cursor: v.optional(v.string()),
|
|
68
|
+
limit: v.number(),
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Report format validator.
|
|
73
|
+
*/
|
|
74
|
+
export const vReportFormat = v.union(
|
|
75
|
+
v.literal("json"),
|
|
76
|
+
v.literal("csv")
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Anomaly pattern validator.
|
|
81
|
+
*/
|
|
82
|
+
export const vAnomalyPattern = v.object({
|
|
83
|
+
action: v.string(),
|
|
84
|
+
threshold: v.number(),
|
|
85
|
+
windowMinutes: v.number(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Cleanup options validator.
|
|
90
|
+
*/
|
|
91
|
+
export const vCleanupOptions = v.object({
|
|
92
|
+
olderThanDays: v.optional(v.number()),
|
|
93
|
+
preserveSeverity: v.optional(v.array(vSeverity)),
|
|
94
|
+
retentionCategory: v.optional(v.string()),
|
|
95
|
+
batchSize: v.optional(v.number()),
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Configuration options validator.
|
|
100
|
+
*/
|
|
101
|
+
export const vConfigOptions = v.object({
|
|
102
|
+
defaultRetentionDays: v.optional(v.number()),
|
|
103
|
+
criticalRetentionDays: v.optional(v.number()),
|
|
104
|
+
piiFieldsToRedact: v.optional(v.array(v.string())),
|
|
105
|
+
samplingEnabled: v.optional(v.boolean()),
|
|
106
|
+
samplingRate: v.optional(v.number()),
|
|
107
|
+
customRetention: v.optional(
|
|
108
|
+
v.array(
|
|
109
|
+
v.object({
|
|
110
|
+
category: v.string(),
|
|
111
|
+
retentionDays: v.number(),
|
|
112
|
+
})
|
|
113
|
+
)
|
|
114
|
+
),
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
export type Severity = Infer<typeof vSeverity>;
|
|
118
|
+
export type AuditEventInput = Infer<typeof vAuditEventInput>;
|
|
119
|
+
export type ChangeEventInput = Infer<typeof vChangeEventInput>;
|
|
120
|
+
export type QueryFilters = Infer<typeof vQueryFilters>;
|
|
121
|
+
export type Pagination = Infer<typeof vPagination>;
|
|
122
|
+
export type ReportFormat = Infer<typeof vReportFormat>;
|
|
123
|
+
export type AnomalyPattern = Infer<typeof vAnomalyPattern>;
|
|
124
|
+
export type CleanupOptions = Infer<typeof vCleanupOptions>;
|
|
125
|
+
export type ConfigOptions = Infer<typeof vConfigOptions>;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Full audit event with generated fields.
|
|
129
|
+
*/
|
|
130
|
+
export type AuditEvent = AuditEventInput & {
|
|
131
|
+
_id: string;
|
|
132
|
+
_creationTime: number;
|
|
133
|
+
timestamp: number;
|
|
134
|
+
before?: unknown;
|
|
135
|
+
after?: unknown;
|
|
136
|
+
diff?: string;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Paginated result type.
|
|
141
|
+
*/
|
|
142
|
+
export type PaginatedResult<T> = {
|
|
143
|
+
items: T[];
|
|
144
|
+
cursor: string | null;
|
|
145
|
+
hasMore: boolean;
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Anomaly detection result.
|
|
150
|
+
*/
|
|
151
|
+
export type Anomaly = {
|
|
152
|
+
action: string;
|
|
153
|
+
count: number;
|
|
154
|
+
threshold: number;
|
|
155
|
+
windowMinutes: number;
|
|
156
|
+
detectedAt: number;
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Report result type.
|
|
161
|
+
*/
|
|
162
|
+
export type Report = {
|
|
163
|
+
format: ReportFormat;
|
|
164
|
+
data: string;
|
|
165
|
+
generatedAt: number;
|
|
166
|
+
recordCount: number;
|
|
167
|
+
};
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useQuery } from "convex/react";
|
|
4
|
+
import type { FunctionReference } from "convex/server";
|
|
5
|
+
|
|
6
|
+
export type Severity = "info" | "warning" | "error" | "critical";
|
|
7
|
+
|
|
8
|
+
export interface AuditLogEntry {
|
|
9
|
+
_id: string;
|
|
10
|
+
_creationTime: number;
|
|
11
|
+
action: string;
|
|
12
|
+
actorId?: string;
|
|
13
|
+
timestamp: number;
|
|
14
|
+
resourceType?: string;
|
|
15
|
+
resourceId?: string;
|
|
16
|
+
metadata?: unknown;
|
|
17
|
+
severity: Severity;
|
|
18
|
+
ipAddress?: string;
|
|
19
|
+
userAgent?: string;
|
|
20
|
+
sessionId?: string;
|
|
21
|
+
tags?: string[];
|
|
22
|
+
before?: unknown;
|
|
23
|
+
after?: unknown;
|
|
24
|
+
diff?: string;
|
|
25
|
+
retentionCategory?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface AuditLogStats {
|
|
29
|
+
totalCount: number;
|
|
30
|
+
bySeverity: {
|
|
31
|
+
info: number;
|
|
32
|
+
warning: number;
|
|
33
|
+
error: number;
|
|
34
|
+
critical: number;
|
|
35
|
+
};
|
|
36
|
+
topActions: { action: string; count: number }[];
|
|
37
|
+
topActors: { actorId: string; count: number }[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface Anomaly {
|
|
41
|
+
action: string;
|
|
42
|
+
count: number;
|
|
43
|
+
threshold: number;
|
|
44
|
+
windowMinutes: number;
|
|
45
|
+
detectedAt: number;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Hook to query audit logs by resource.
|
|
50
|
+
* Provides real-time updates when the audit log changes.
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* ```tsx
|
|
54
|
+
* import { useAuditLogByResource } from "convex-audit-log/react";
|
|
55
|
+
* import { api } from "../convex/_generated/api";
|
|
56
|
+
*
|
|
57
|
+
* function DocumentHistory({ documentId }: { documentId: string }) {
|
|
58
|
+
* const logs = useAuditLogByResource(api.auditLog.queryByResource, {
|
|
59
|
+
* resourceType: "documents",
|
|
60
|
+
* resourceId: documentId,
|
|
61
|
+
* limit: 20,
|
|
62
|
+
* });
|
|
63
|
+
*
|
|
64
|
+
* if (!logs) return <div>Loading...</div>;
|
|
65
|
+
*
|
|
66
|
+
* return (
|
|
67
|
+
* <ul>
|
|
68
|
+
* {logs.map((log) => (
|
|
69
|
+
* <li key={log._id}>
|
|
70
|
+
* {log.action} by {log.actorId} at {new Date(log.timestamp).toLocaleString()}
|
|
71
|
+
* </li>
|
|
72
|
+
* ))}
|
|
73
|
+
* </ul>
|
|
74
|
+
* );
|
|
75
|
+
* }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
export function useAuditLogByResource(
|
|
79
|
+
queryRef: FunctionReference<
|
|
80
|
+
"query",
|
|
81
|
+
"public",
|
|
82
|
+
{ resourceType: string; resourceId: string; limit?: number; fromTimestamp?: number },
|
|
83
|
+
AuditLogEntry[]
|
|
84
|
+
>,
|
|
85
|
+
args: {
|
|
86
|
+
resourceType: string;
|
|
87
|
+
resourceId: string;
|
|
88
|
+
limit?: number;
|
|
89
|
+
fromTimestamp?: number;
|
|
90
|
+
}
|
|
91
|
+
): AuditLogEntry[] | undefined {
|
|
92
|
+
return useQuery(queryRef, args);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Hook to query audit logs by actor (user).
|
|
97
|
+
*
|
|
98
|
+
* @example
|
|
99
|
+
* ```tsx
|
|
100
|
+
* import { useAuditLogByActor } from "convex-audit-log/react";
|
|
101
|
+
* import { api } from "../convex/_generated/api";
|
|
102
|
+
*
|
|
103
|
+
* function UserActivity({ userId }: { userId: string }) {
|
|
104
|
+
* const logs = useAuditLogByActor(api.auditLog.queryByActor, {
|
|
105
|
+
* actorId: userId,
|
|
106
|
+
* limit: 50,
|
|
107
|
+
* });
|
|
108
|
+
*
|
|
109
|
+
* if (!logs) return <div>Loading...</div>;
|
|
110
|
+
*
|
|
111
|
+
* return (
|
|
112
|
+
* <ul>
|
|
113
|
+
* {logs.map((log) => (
|
|
114
|
+
* <li key={log._id}>{log.action}</li>
|
|
115
|
+
* ))}
|
|
116
|
+
* </ul>
|
|
117
|
+
* );
|
|
118
|
+
* }
|
|
119
|
+
* ```
|
|
120
|
+
*/
|
|
121
|
+
export function useAuditLogByActor(
|
|
122
|
+
queryRef: FunctionReference<
|
|
123
|
+
"query",
|
|
124
|
+
"public",
|
|
125
|
+
{ actorId: string; limit?: number; fromTimestamp?: number; actions?: string[] },
|
|
126
|
+
AuditLogEntry[]
|
|
127
|
+
>,
|
|
128
|
+
args: {
|
|
129
|
+
actorId: string;
|
|
130
|
+
limit?: number;
|
|
131
|
+
fromTimestamp?: number;
|
|
132
|
+
actions?: string[];
|
|
133
|
+
}
|
|
134
|
+
): AuditLogEntry[] | undefined {
|
|
135
|
+
return useQuery(queryRef, args);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Hook to watch critical events in real-time.
|
|
140
|
+
*
|
|
141
|
+
* @example
|
|
142
|
+
* ```tsx
|
|
143
|
+
* import { useWatchCriticalEvents } from "convex-audit-log/react";
|
|
144
|
+
* import { api } from "../convex/_generated/api";
|
|
145
|
+
*
|
|
146
|
+
* function SecurityAlerts() {
|
|
147
|
+
* const criticalEvents = useWatchCriticalEvents(api.auditLog.watchCritical, {
|
|
148
|
+
* limit: 10,
|
|
149
|
+
* });
|
|
150
|
+
*
|
|
151
|
+
* if (!criticalEvents) return <div>Loading...</div>;
|
|
152
|
+
*
|
|
153
|
+
* return (
|
|
154
|
+
* <div className="alerts">
|
|
155
|
+
* {criticalEvents.map((event) => (
|
|
156
|
+
* <div key={event._id} className={`alert alert-${event.severity}`}>
|
|
157
|
+
* {event.action}
|
|
158
|
+
* </div>
|
|
159
|
+
* ))}
|
|
160
|
+
* </div>
|
|
161
|
+
* );
|
|
162
|
+
* }
|
|
163
|
+
* ```
|
|
164
|
+
*/
|
|
165
|
+
export function useWatchCriticalEvents(
|
|
166
|
+
queryRef: FunctionReference<
|
|
167
|
+
"query",
|
|
168
|
+
"public",
|
|
169
|
+
{ severity?: Severity[]; limit?: number },
|
|
170
|
+
AuditLogEntry[]
|
|
171
|
+
>,
|
|
172
|
+
args?: {
|
|
173
|
+
severity?: Severity[];
|
|
174
|
+
limit?: number;
|
|
175
|
+
}
|
|
176
|
+
): AuditLogEntry[] | undefined {
|
|
177
|
+
return useQuery(queryRef, args ?? {});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Hook to get audit log statistics.
|
|
182
|
+
*
|
|
183
|
+
* @example
|
|
184
|
+
* ```tsx
|
|
185
|
+
* import { useAuditLogStats } from "convex-audit-log/react";
|
|
186
|
+
* import { api } from "../convex/_generated/api";
|
|
187
|
+
*
|
|
188
|
+
* function Dashboard() {
|
|
189
|
+
* const stats = useAuditLogStats(api.auditLog.getStats, {
|
|
190
|
+
* fromTimestamp: Date.now() - 24 * 60 * 60 * 1000, // Last 24 hours
|
|
191
|
+
* });
|
|
192
|
+
*
|
|
193
|
+
* if (!stats) return <div>Loading...</div>;
|
|
194
|
+
*
|
|
195
|
+
* return (
|
|
196
|
+
* <div>
|
|
197
|
+
* <h2>Total Events: {stats.totalCount}</h2>
|
|
198
|
+
* <div>Critical: {stats.bySeverity.critical}</div>
|
|
199
|
+
* <div>Errors: {stats.bySeverity.error}</div>
|
|
200
|
+
* </div>
|
|
201
|
+
* );
|
|
202
|
+
* }
|
|
203
|
+
* ```
|
|
204
|
+
*/
|
|
205
|
+
export function useAuditLogStats(
|
|
206
|
+
queryRef: FunctionReference<
|
|
207
|
+
"query",
|
|
208
|
+
"public",
|
|
209
|
+
{ fromTimestamp?: number; toTimestamp?: number },
|
|
210
|
+
AuditLogStats
|
|
211
|
+
>,
|
|
212
|
+
args?: {
|
|
213
|
+
fromTimestamp?: number;
|
|
214
|
+
toTimestamp?: number;
|
|
215
|
+
}
|
|
216
|
+
): AuditLogStats | undefined {
|
|
217
|
+
return useQuery(queryRef, args ?? {});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Hook to detect anomalies in real-time.
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* ```tsx
|
|
225
|
+
* import { useAnomalyDetection } from "convex-audit-log/react";
|
|
226
|
+
* import { api } from "../convex/_generated/api";
|
|
227
|
+
*
|
|
228
|
+
* function AnomalyMonitor() {
|
|
229
|
+
* const anomalies = useAnomalyDetection(api.auditLog.detectAnomalies, {
|
|
230
|
+
* patterns: [
|
|
231
|
+
* { action: "user.login.failed", threshold: 5, windowMinutes: 5 },
|
|
232
|
+
* { action: "record.deleted", threshold: 10, windowMinutes: 1 },
|
|
233
|
+
* ],
|
|
234
|
+
* });
|
|
235
|
+
*
|
|
236
|
+
* if (!anomalies) return <div>Loading...</div>;
|
|
237
|
+
* if (anomalies.length === 0) return <div>No anomalies detected</div>;
|
|
238
|
+
*
|
|
239
|
+
* return (
|
|
240
|
+
* <div className="anomalies">
|
|
241
|
+
* {anomalies.map((anomaly, i) => (
|
|
242
|
+
* <div key={i} className="anomaly-alert">
|
|
243
|
+
* {anomaly.action}: {anomaly.count} events in {anomaly.windowMinutes} minutes
|
|
244
|
+
* (threshold: {anomaly.threshold})
|
|
245
|
+
* </div>
|
|
246
|
+
* ))}
|
|
247
|
+
* </div>
|
|
248
|
+
* );
|
|
249
|
+
* }
|
|
250
|
+
* ```
|
|
251
|
+
*/
|
|
252
|
+
export function useAnomalyDetection(
|
|
253
|
+
queryRef: FunctionReference<
|
|
254
|
+
"query",
|
|
255
|
+
"public",
|
|
256
|
+
{ patterns: { action: string; threshold: number; windowMinutes: number }[] },
|
|
257
|
+
Anomaly[]
|
|
258
|
+
>,
|
|
259
|
+
args: {
|
|
260
|
+
patterns: { action: string; threshold: number; windowMinutes: number }[];
|
|
261
|
+
}
|
|
262
|
+
): Anomaly[] | undefined {
|
|
263
|
+
return useQuery(queryRef, args);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Formats a timestamp to a human-readable string.
|
|
268
|
+
*/
|
|
269
|
+
export function formatTimestamp(timestamp: number): string {
|
|
270
|
+
return new Date(timestamp).toLocaleString();
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Returns a CSS class name based on severity.
|
|
275
|
+
*/
|
|
276
|
+
export function getSeverityClass(severity: Severity): string {
|
|
277
|
+
switch (severity) {
|
|
278
|
+
case "critical":
|
|
279
|
+
return "audit-severity-critical";
|
|
280
|
+
case "error":
|
|
281
|
+
return "audit-severity-error";
|
|
282
|
+
case "warning":
|
|
283
|
+
return "audit-severity-warning";
|
|
284
|
+
case "info":
|
|
285
|
+
default:
|
|
286
|
+
return "audit-severity-info";
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Returns a color based on severity (for inline styles).
|
|
292
|
+
*/
|
|
293
|
+
export function getSeverityColor(severity: Severity): string {
|
|
294
|
+
switch (severity) {
|
|
295
|
+
case "critical":
|
|
296
|
+
return "#dc2626"; // red-600
|
|
297
|
+
case "error":
|
|
298
|
+
return "#ea580c"; // orange-600
|
|
299
|
+
case "warning":
|
|
300
|
+
return "#ca8a04"; // yellow-600
|
|
301
|
+
case "info":
|
|
302
|
+
default:
|
|
303
|
+
return "#2563eb"; // blue-600
|
|
304
|
+
}
|
|
305
|
+
}
|
package/src/test.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/// <reference types="vite/client" />
|
|
2
|
+
import type { TestConvex } from "convex-test";
|
|
3
|
+
import type { GenericSchema, SchemaDefinition } from "convex/server";
|
|
4
|
+
import schema from "./component/schema.js";
|
|
5
|
+
const modules = import.meta.glob("./component/**/*.ts");
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Register the component with the test convex instance.
|
|
9
|
+
* @param t - The test convex instance, e.g. from calling `convexTest`.
|
|
10
|
+
* @param name - The name of the component, as registered in convex.config.ts.
|
|
11
|
+
*/
|
|
12
|
+
export function register(
|
|
13
|
+
t: TestConvex<SchemaDefinition<GenericSchema, boolean>>,
|
|
14
|
+
name: string = "auditLog",
|
|
15
|
+
) {
|
|
16
|
+
t.registerComponent(name, schema, modules);
|
|
17
|
+
}
|
|
18
|
+
export default { register, schema, modules };
|