@goplusvn/core 0.1.53 → 0.1.55

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.
@@ -1,209 +1,14 @@
1
1
  "use client";
2
2
 
3
- import React, { useState, useEffect } from "react";
4
- import { auditManager } from "../../audit/audit-manager";
5
- import type { AuditLog } from "../../audit/types";
6
-
7
- // Relative imports to core UI components
8
- import { Badge } from "../primitives/badge";
9
- import { Button } from "../primitives/button";
10
- import { Card, CardHeader, CardTitle, CardContent } from "../primitives/card";
11
- import { Input } from "../primitives/input";
12
- import { Label } from "../primitives/label";
13
- import {
14
- Table,
15
- TableBody,
16
- TableCell,
17
- TableHead,
18
- TableHeader,
19
- TableRow,
20
- } from "../primitives/table";
21
- import {
22
- Select,
23
- SelectContent,
24
- SelectItem,
25
- SelectTrigger,
26
- SelectValue,
27
- } from "../primitives/select";
28
-
29
- export const AuditLogPage = () => {
30
- const [logs, setLogs] = useState<AuditLog[]>([]);
31
- const [userIdFilter, setUserIdFilter] = useState("");
32
- const [actionFilter, setActionFilter] = useState("");
33
- const [resourceFilter, setResourceFilter] = useState("");
34
- const [typeFilter, setTypeFilter] = useState<string>("all");
35
-
36
- const fetchLogs = () => {
37
- // Safe cast as we know we added getLogs
38
- // In real app, we would have proper interface
39
- const manager = auditManager as any;
40
- if (typeof manager.getLogs === "function") {
41
- const filter: any = {};
42
- if (userIdFilter) filter.userId = userIdFilter;
43
- if (actionFilter) filter.action = actionFilter;
44
- if (resourceFilter) filter.resource = resourceFilter;
45
- if (typeFilter && typeFilter !== "all") filter.type = typeFilter;
46
-
47
- setLogs(manager.getLogs(filter));
48
- }
49
- };
50
-
51
- useEffect(() => {
52
- fetchLogs();
53
- // Poll for updates in demo
54
- const interval = setInterval(fetchLogs, 5000);
55
- return () => clearInterval(interval);
56
- }, [userIdFilter, actionFilter, resourceFilter, typeFilter]);
57
-
58
- // Format changes object for display
59
- const renderChanges = (changes: any) => {
60
- if (!changes) return <span className="text-muted-foreground">-</span>;
61
- return (
62
- <code
63
- className="text-xs bg-slate-100 dark:bg-slate-800 p-1 rounded block max-w-[300px] overflow-hidden text-ellipsis whitespace-nowrap"
64
- title={JSON.stringify(changes, null, 2)}
65
- >
66
- {Object.keys(changes).join(", ")}
67
- </code>
68
- );
69
- };
70
-
71
- return (
72
- <div className="p-6 space-y-6 h-full flex flex-col">
73
- <div className="flex justify-between items-center mb-4">
74
- <div>
75
- <h1 className="text-2xl font-bold tracking-tight">Audit Logs</h1>
76
- <p className="text-muted-foreground text-sm">
77
- Track system changes and user actions.
78
- </p>
79
- </div>
80
- <Button onClick={fetchLogs} variant="outline">
81
- Refresh
82
- </Button>
83
- </div>
84
-
85
- <Card>
86
- <CardHeader>
87
- <CardTitle className="text-base font-medium">Filters</CardTitle>
88
- </CardHeader>
89
- <CardContent>
90
- <div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-4 gap-4">
91
- <div className="space-y-1">
92
- <Label htmlFor="user-filter">User ID</Label>
93
- <Input
94
- id="user-filter"
95
- placeholder="Search by user..."
96
- value={userIdFilter}
97
- onChange={(e) => setUserIdFilter(e.target.value)}
98
- />
99
- </div>
100
- <div className="space-y-1">
101
- <Label htmlFor="action-filter">Action</Label>
102
- <Input
103
- id="action-filter"
104
- placeholder="e.g. create, update"
105
- value={actionFilter}
106
- onChange={(e) => setActionFilter(e.target.value)}
107
- />
108
- </div>
109
- <div className="space-y-1">
110
- <Label htmlFor="resource-filter">Resource</Label>
111
- <Input
112
- id="resource-filter"
113
- placeholder="e.g. product, order"
114
- value={resourceFilter}
115
- onChange={(e) => setResourceFilter(e.target.value)}
116
- />
117
- </div>
118
- <div className="space-y-1">
119
- <Label htmlFor="type-filter">Type</Label>
120
- <Select value={typeFilter} onValueChange={setTypeFilter}>
121
- <SelectTrigger>
122
- <SelectValue placeholder="All Types" />
123
- </SelectTrigger>
124
- <SelectContent>
125
- <SelectItem value="all">All Types</SelectItem>
126
- <SelectItem value="info">Info</SelectItem>
127
- <SelectItem value="warning">Warning</SelectItem>
128
- <SelectItem value="error">Error</SelectItem>
129
- </SelectContent>
130
- </Select>
131
- </div>
132
- </div>
133
- </CardContent>
134
- </Card>
135
-
136
- <Card className="flex-1 flex flex-col min-h-0">
137
- <CardContent className="flex-1 overflow-auto p-0">
138
- <Table>
139
- <TableHeader>
140
- <TableRow>
141
- <TableHead className="w-[180px]">Timestamp</TableHead>
142
- <TableHead className="w-[150px]">Action</TableHead>
143
- <TableHead className="w-[150px]">Resource</TableHead>
144
- <TableHead className="w-[150px]">User</TableHead>
145
- <TableHead>Changes / Metadata</TableHead>
146
- <TableHead className="w-[100px] text-right">Status</TableHead>
147
- </TableRow>
148
- </TableHeader>
149
- <TableBody>
150
- {logs.length === 0 && (
151
- <TableRow>
152
- <TableCell
153
- colSpan={6}
154
- className="text-center h-24 text-muted-foreground"
155
- >
156
- No logs found matching filters.
157
- </TableCell>
158
- </TableRow>
159
- )}
160
- {logs.map((log) => (
161
- <TableRow key={log.id}>
162
- <TableCell className="font-mono text-xs">
163
- {new Date(log.createdAt).toLocaleString()}
164
- </TableCell>
165
- <TableCell>
166
- <Badge variant="outline" className="font-normal">
167
- {log.action}
168
- </Badge>
169
- </TableCell>
170
- <TableCell>{log.resource}</TableCell>
171
- <TableCell>
172
- <div className="flex flex-col">
173
- <span>{log.userId || "System"}</span>
174
- {log.roleName && (
175
- <span className="text-[10px] text-muted-foreground">
176
- {log.roleName}
177
- </span>
178
- )}
179
- </div>
180
- </TableCell>
181
- <TableCell>
182
- {renderChanges(log.changes)}
183
- {log.metadata && Object.keys(log.metadata).length > 0 && (
184
- <div className="text-[10px] text-muted-foreground mt-1">
185
- MD: {JSON.stringify(log.metadata).substring(0, 50)}...
186
- </div>
187
- )}
188
- </TableCell>
189
- <TableCell className="text-right">
190
- {(log.status || 200) >= 400 ? (
191
- <Badge variant="destructive">{log.status}</Badge>
192
- ) : (
193
- <Badge
194
- variant="secondary"
195
- className="bg-emerald-100 text-emerald-700 hover:bg-emerald-100"
196
- >
197
- {log.status || 200}
198
- </Badge>
199
- )}
200
- </TableCell>
201
- </TableRow>
202
- ))}
203
- </TableBody>
204
- </Table>
205
- </CardContent>
206
- </Card>
207
- </div>
208
- );
209
- };
3
+ /**
4
+ * @deprecated Dùng `@goerp/core/system/pages/system-audit-page` (`SystemAuditPage`).
5
+ *
6
+ * Bản cũ ở đây đọc `auditManager` trong RAM ngay trong CLIENT component — luôn
7
+ * rỗng, dữ liệu audit nằm ở server. Nay trỏ thẳng sang trang thật (đọc bảng
8
+ * `audit_logs` qua API) để app đang import tên `AuditLogPage` không phải sửa
9
+ * ngay; app mới hãy import thẳng `SystemAuditPage`.
10
+ */
11
+ export {
12
+ SystemAuditPage as AuditLogPage,
13
+ type SystemAuditPageProps as AuditLogPageProps,
14
+ } from "../../system/pages/system-audit-page";
@@ -1,139 +0,0 @@
1
- import type {
2
- AuditLog,
3
- AuditLogger,
4
- AuditManagerOptions,
5
- CreateAuditInput,
6
- } from "./types";
7
- import { createLogger } from "../infrastructure/logger";
8
- import { MemoryAuditLogger } from "./memory-audit-logger";
9
-
10
- const logger = createLogger("AuditManager");
11
-
12
- /**
13
- * Console-based audit logger (default)
14
- * In production, replace with database or external service
15
- */
16
- class ConsoleAuditLogger implements AuditLogger {
17
- async log(entry: AuditLog): Promise<void> {
18
- logger.info(`AUDIT: ${entry.action} ${entry.resource}`, {
19
- id: entry.id,
20
- resourceId: entry.resourceId,
21
- userId: entry.userId,
22
- changes: entry.changes,
23
- });
24
- }
25
- }
26
-
27
- /**
28
- * AuditManager - tracks actions for compliance and debugging
29
- *
30
- * @example
31
- * ```typescript
32
- * import { auditManager } from '@goerp/core/audit';
33
- *
34
- * // Log an action
35
- * await auditManager.log({
36
- * action: 'update',
37
- * resource: 'purchase-order',
38
- * resourceId: '123',
39
- * userId: session.user.id,
40
- * changes: { status: { old: 'pending', new: 'approved' } }
41
- * });
42
- * ```
43
- */
44
- class AuditManagerImpl {
45
- private auditLogger: AuditLogger;
46
- private registeredActions: Set<string>;
47
-
48
- constructor(options: AuditManagerOptions = {}) {
49
- this.auditLogger = options.logger || new MemoryAuditLogger();
50
- this.registeredActions = new Set(
51
- options.actions || ["create", "update", "delete", "login", "logout"],
52
- );
53
- }
54
-
55
- /** Set custom audit logger */
56
- setLogger(auditLogger: AuditLogger): void {
57
- this.auditLogger = auditLogger;
58
- }
59
-
60
- /** Register action for automatic auditing */
61
- registerAction(action: string): void {
62
- this.registeredActions.add(action);
63
- }
64
-
65
- /** Check if action is registered for auditing */
66
- isRegistered(action: string): boolean {
67
- return this.registeredActions.has(action);
68
- }
69
-
70
- /** Log an audit entry */
71
- async log(input: CreateAuditInput): Promise<void> {
72
- const entry: AuditLog = {
73
- id: crypto.randomUUID(),
74
- ...input,
75
- createdAt: new Date(),
76
- };
77
-
78
- try {
79
- await this.auditLogger.log(entry);
80
- } catch (error) {
81
- logger.error("Failed to log audit entry", { error: String(error) });
82
- }
83
- }
84
-
85
- /**
86
- * Get logs if the logger supports it (specifically MemoryAuditLogger)
87
- */
88
- getLogs(filter: any = {}): AuditLog[] {
89
- if ("getLogs" in this.auditLogger) {
90
- return (this.auditLogger as any).getLogs(filter);
91
- }
92
- return [];
93
- }
94
-
95
- /** Create middleware for automatic API route auditing */
96
- middleware() {
97
- return async (
98
- ctx: {
99
- action?: { actionName: string; resourceName: string };
100
- state?: { currentUser?: { id: string }; currentRole?: string };
101
- request?: { ip?: string; header?: Record<string, string> };
102
- status?: number;
103
- },
104
- next: () => Promise<void>,
105
- ): Promise<void> => {
106
- const startTime = Date.now();
107
- let error: Error | null = null;
108
-
109
- try {
110
- await next();
111
- } catch (e) {
112
- error = e as Error;
113
- throw e;
114
- } finally {
115
- if (ctx.action && this.isRegistered(ctx.action.actionName)) {
116
- await this.log({
117
- action: ctx.action.actionName,
118
- resource: ctx.action.resourceName,
119
- userId: ctx.state?.currentUser?.id,
120
- roleName: ctx.state?.currentRole,
121
- ip: ctx.request?.ip,
122
- userAgent: ctx.request?.header?.["user-agent"],
123
- status: ctx.status || (error ? 500 : 200),
124
- metadata: {
125
- duration: Date.now() - startTime,
126
- ...(error && { error: error.message }),
127
- },
128
- });
129
- }
130
- }
131
- };
132
- }
133
- }
134
-
135
- // Singleton instance
136
- export const auditManager = new AuditManagerImpl();
137
-
138
- // Export class for testing
139
- export { AuditManagerImpl, ConsoleAuditLogger, MemoryAuditLogger };
@@ -1,86 +0,0 @@
1
- import type { AuditLog, AuditLogger } from "./types";
2
-
3
- /**
4
- * Filter options for retrieving audit logs
5
- */
6
- export interface AuditLogFilter {
7
- fromDate?: Date;
8
- toDate?: Date;
9
- userId?: string;
10
- action?: string;
11
- resource?: string;
12
- type?: "info" | "warning" | "error";
13
- limit?: number;
14
- offset?: number;
15
- }
16
-
17
- /**
18
- * In-memory audit logger that supports retrieval
19
- */
20
- export class MemoryAuditLogger implements AuditLogger {
21
- private logs: AuditLog[] = [];
22
- private readonly maxLogs: number;
23
-
24
- constructor(maxLogs = 1000) {
25
- this.maxLogs = maxLogs;
26
- }
27
-
28
- async log(entry: AuditLog): Promise<void> {
29
- // Add to beginning of array
30
- this.logs.unshift(entry);
31
-
32
- // Trim if exceeds max size
33
- if (this.logs.length > this.maxLogs) {
34
- this.logs = this.logs.slice(0, this.maxLogs);
35
- }
36
- }
37
-
38
- /**
39
- * Get logs with filtering
40
- */
41
- getLogs(filter: AuditLogFilter = {}): AuditLog[] {
42
- let filtered = this.logs;
43
-
44
- if (filter.fromDate) {
45
- filtered = filtered.filter((log) => log.createdAt >= filter.fromDate!);
46
- }
47
-
48
- if (filter.toDate) {
49
- filtered = filtered.filter((log) => log.createdAt <= filter.toDate!);
50
- }
51
-
52
- if (filter.userId) {
53
- filtered = filtered.filter((log) => log.userId === filter.userId);
54
- }
55
-
56
- if (filter.action) {
57
- filtered = filtered.filter((log) => log.action === filter.action);
58
- }
59
-
60
- if (filter.resource) {
61
- filtered = filtered.filter((log) => log.resource === filter.resource);
62
- }
63
-
64
- // "type" filter is a fuzzy mapping based on status code or action name for demo purposes
65
- if (filter.type) {
66
- filtered = filtered.filter((log) => {
67
- if (filter.type === "error") return (log.status || 200) >= 400;
68
- if (filter.type === "warning")
69
- return (log.status || 200) >= 300 && (log.status || 200) < 400;
70
- return (log.status || 200) < 300;
71
- });
72
- }
73
-
74
- const offset = filter.offset || 0;
75
- const limit = filter.limit || 50;
76
-
77
- return filtered.slice(offset, offset + limit);
78
- }
79
-
80
- /**
81
- * Clear all logs
82
- */
83
- clear(): void {
84
- this.logs = [];
85
- }
86
- }
@@ -1,50 +0,0 @@
1
- // Audit Types
2
- export interface AuditLog {
3
- id: string;
4
- /** Action type: create, update, delete, login, logout, custom */
5
- action: string;
6
- /** Resource/entity name (e.g., 'purchase-order', 'user') */
7
- resource: string;
8
- /** Resource ID */
9
- resourceId?: string;
10
- /** User who performed the action */
11
- userId?: string;
12
- /** User's role at time of action */
13
- roleName?: string;
14
- /** Changes made (for update actions) */
15
- changes?: Record<string, { old: unknown; new: unknown }>;
16
- /** Additional metadata */
17
- metadata?: Record<string, unknown>;
18
- /** Request IP address */
19
- ip?: string;
20
- /** User agent */
21
- userAgent?: string;
22
- /** HTTP status code */
23
- status?: number;
24
- /** Timestamp */
25
- createdAt: Date;
26
- }
27
-
28
- export interface CreateAuditInput {
29
- action: string;
30
- resource: string;
31
- resourceId?: string;
32
- userId?: string;
33
- roleName?: string;
34
- changes?: Record<string, { old: unknown; new: unknown }>;
35
- metadata?: Record<string, unknown>;
36
- ip?: string;
37
- userAgent?: string;
38
- status?: number;
39
- }
40
-
41
- export interface AuditLogger {
42
- log(entry: AuditLog): Promise<void>;
43
- }
44
-
45
- export interface AuditManagerOptions {
46
- /** Custom logger implementation */
47
- logger?: AuditLogger;
48
- /** Actions to automatically audit */
49
- actions?: string[];
50
- }