Skip to content

Commit 1e2cb59

Browse files
committed
system objects
1 parent 55c7c59 commit 1e2cb59

File tree

8 files changed

+395
-238
lines changed

8 files changed

+395
-238
lines changed

packages/plugins/plugin-audit/src/objects/sys-audit-log.object.ts

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,9 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
88
* Immutable audit trail for all significant platform events.
99
* Records who did what, when, and the before/after state.
1010
*
11+
* Every field is `readonly: true` — audit logs are written only by
12+
* internal system hooks, never via UI forms. API exposes only `get` + `list`.
13+
*
1114
* @namespace sys
1215
*/
1316
export const SysAuditLog = ObjectSchema.create({
@@ -18,97 +21,133 @@ export const SysAuditLog = ObjectSchema.create({
1821
icon: 'scroll-text',
1922
isSystem: true,
2023
description: 'Immutable audit trail for platform events',
21-
titleFormat: '{action} on {object_name} by {user_id}',
22-
compactLayout: ['action', 'object_name', 'user_id', 'created_at'],
23-
24+
displayNameField: 'action',
25+
titleFormat: '{action} · {object_name}',
26+
compactLayout: ['created_at', 'action', 'object_name', 'record_id', 'user_id'],
27+
2428
fields: {
25-
id: Field.text({
26-
label: 'Audit Log ID',
27-
required: true,
28-
readonly: true,
29-
}),
30-
29+
// ── Event ────────────────────────────────────────────────────
3130
created_at: Field.datetime({
3231
label: 'Timestamp',
3332
required: true,
3433
defaultValue: 'NOW()',
3534
readonly: true,
35+
group: 'Event',
3636
}),
37-
37+
38+
action: Field.select(
39+
['create', 'update', 'delete', 'restore', 'login', 'logout', 'permission_change', 'config_change', 'export', 'import'],
40+
{
41+
label: 'Action',
42+
required: true,
43+
readonly: true,
44+
searchable: true,
45+
description: 'Action type (snake_case)',
46+
group: 'Event',
47+
},
48+
),
49+
3850
user_id: Field.text({
39-
label: 'User ID',
51+
label: 'Actor',
4052
required: false,
53+
readonly: true,
54+
searchable: true,
4155
description: 'User who performed the action (null for system actions)',
56+
group: 'Event',
4257
}),
43-
44-
action: Field.select(['create', 'update', 'delete', 'restore', 'login', 'logout', 'permission_change', 'config_change', 'export', 'import'], {
45-
label: 'Action',
46-
required: true,
47-
description: 'Action type (snake_case). Values: create, update, delete, restore, login, logout, permission_change, config_change, export, import',
48-
}),
49-
58+
59+
// ── Target record ────────────────────────────────────────────
5060
object_name: Field.text({
51-
label: 'Object Name',
61+
label: 'Object',
5262
required: false,
63+
readonly: true,
64+
searchable: true,
5365
maxLength: 255,
5466
description: 'Target object (e.g. sys_user, project_task)',
67+
group: 'Target',
5568
}),
56-
69+
5770
record_id: Field.text({
5871
label: 'Record ID',
5972
required: false,
73+
readonly: true,
74+
searchable: true,
6075
description: 'ID of the affected record',
76+
group: 'Target',
6177
}),
62-
78+
79+
// ── Change payload ───────────────────────────────────────────
6380
old_value: Field.textarea({
6481
label: 'Old Value',
6582
required: false,
83+
readonly: true,
6684
description: 'JSON-serialized previous state',
85+
group: 'Changes',
6786
}),
68-
87+
6988
new_value: Field.textarea({
7089
label: 'New Value',
7190
required: false,
91+
readonly: true,
7292
description: 'JSON-serialized new state',
93+
group: 'Changes',
7394
}),
74-
95+
96+
// ── Client fingerprint ───────────────────────────────────────
7597
ip_address: Field.text({
7698
label: 'IP Address',
7799
required: false,
100+
readonly: true,
78101
maxLength: 45,
102+
group: 'Client',
79103
}),
80-
104+
81105
user_agent: Field.textarea({
82106
label: 'User Agent',
83107
required: false,
108+
readonly: true,
109+
group: 'Client',
84110
}),
85-
111+
112+
// ── Context ──────────────────────────────────────────────────
86113
tenant_id: Field.text({
87-
label: 'Tenant ID',
114+
label: 'Tenant',
88115
required: false,
116+
readonly: true,
89117
description: 'Tenant context for multi-tenant isolation',
118+
group: 'Context',
90119
}),
91-
120+
92121
metadata: Field.textarea({
93122
label: 'Metadata',
94123
required: false,
124+
readonly: true,
95125
description: 'JSON-serialized additional context',
126+
group: 'Context',
127+
}),
128+
129+
// ── System ───────────────────────────────────────────────────
130+
id: Field.text({
131+
label: 'Audit Log ID',
132+
required: true,
133+
readonly: true,
134+
group: 'System',
96135
}),
97136
},
98-
137+
99138
indexes: [
100139
{ fields: ['created_at'] },
101140
{ fields: ['user_id'] },
102141
{ fields: ['object_name', 'record_id'] },
103142
{ fields: ['action'] },
104143
{ fields: ['tenant_id'] },
105144
],
106-
145+
107146
enable: {
108147
trackHistory: false, // Audit logs are themselves the audit trail
109148
searchable: true,
110149
apiEnabled: true,
111-
apiMethods: ['get', 'list'], // Read-only — audit logs are immutable; creation happens via internal system hooks only
150+
apiMethods: ['get', 'list'], // Read-only — creation happens via internal system hooks only
112151
trash: false, // Never soft-delete audit logs
113152
mru: false,
114153
clone: false,

packages/plugins/plugin-auth/src/objects/sys-api-key.object.ts

Lines changed: 63 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ import { ObjectSchema, Field } from '@objectstack/spec/data';
77
*
88
* API keys for programmatic/machine access to the platform.
99
*
10+
* Field `key` stores a hashed value and is marked hidden so it never
11+
* leaks into default list/form rendering; the raw token is only
12+
* returned once on creation via the auth plugin API.
13+
*
1014
* @namespace sys
1115
*/
1216
export const SysApiKey = ObjectSchema.create({
@@ -17,82 +21,105 @@ export const SysApiKey = ObjectSchema.create({
1721
icon: 'key-round',
1822
isSystem: true,
1923
description: 'API keys for programmatic access',
24+
displayNameField: 'name',
2025
titleFormat: '{name}',
21-
compactLayout: ['name', 'user_id', 'expires_at'],
22-
26+
compactLayout: ['name', 'prefix', 'user_id', 'expires_at', 'revoked'],
27+
2328
fields: {
24-
id: Field.text({
25-
label: 'API Key ID',
26-
required: true,
27-
readonly: true,
28-
}),
29-
30-
created_at: Field.datetime({
31-
label: 'Created At',
32-
defaultValue: 'NOW()',
33-
readonly: true,
34-
}),
35-
36-
updated_at: Field.datetime({
37-
label: 'Updated At',
38-
defaultValue: 'NOW()',
39-
readonly: true,
40-
}),
41-
29+
// ── Identity ─────────────────────────────────────────────────
4230
name: Field.text({
4331
label: 'Name',
4432
required: true,
33+
searchable: true,
4534
maxLength: 255,
4635
description: 'Human-readable label for the API key',
36+
group: 'Identity',
4737
}),
48-
49-
key: Field.text({
50-
label: 'Key',
51-
required: true,
52-
description: 'Hashed API key value',
53-
}),
54-
38+
5539
prefix: Field.text({
5640
label: 'Prefix',
5741
required: false,
5842
maxLength: 16,
5943
description: 'Visible prefix for identifying the key (e.g., "osk_")',
44+
group: 'Identity',
6045
}),
61-
46+
6247
user_id: Field.text({
63-
label: 'User ID',
48+
label: 'Owner',
6449
required: true,
65-
description: 'Owner user of this API key',
50+
description: 'User who owns this API key',
51+
group: 'Identity',
6652
}),
67-
53+
54+
// ── Access ───────────────────────────────────────────────────
6855
scopes: Field.textarea({
6956
label: 'Scopes',
7057
required: false,
7158
description: 'JSON array of permission scopes',
59+
group: 'Access',
7260
}),
73-
61+
62+
// ── Lifecycle ────────────────────────────────────────────────
7463
expires_at: Field.datetime({
7564
label: 'Expires At',
7665
required: false,
66+
group: 'Lifecycle',
7767
}),
78-
68+
7969
last_used_at: Field.datetime({
8070
label: 'Last Used At',
8171
required: false,
72+
readonly: true,
73+
description: 'Automatically updated on each API call',
74+
group: 'Lifecycle',
8275
}),
83-
76+
8477
revoked: Field.boolean({
8578
label: 'Revoked',
8679
defaultValue: false,
80+
group: 'Lifecycle',
81+
}),
82+
83+
// ── Secret (hidden by default) ──────────────────────────────
84+
key: Field.text({
85+
label: 'Hashed Key',
86+
required: true,
87+
hidden: true,
88+
readonly: true,
89+
description: 'Hashed API key value — never exposed to clients',
90+
group: 'Secret',
91+
}),
92+
93+
// ── System ───────────────────────────────────────────────────
94+
id: Field.text({
95+
label: 'API Key ID',
96+
required: true,
97+
readonly: true,
98+
group: 'System',
99+
}),
100+
101+
created_at: Field.datetime({
102+
label: 'Created At',
103+
defaultValue: 'NOW()',
104+
readonly: true,
105+
group: 'System',
106+
}),
107+
108+
updated_at: Field.datetime({
109+
label: 'Updated At',
110+
defaultValue: 'NOW()',
111+
readonly: true,
112+
group: 'System',
87113
}),
88114
},
89-
115+
90116
indexes: [
91117
{ fields: ['key'], unique: true },
92118
{ fields: ['user_id'] },
93119
{ fields: ['prefix'] },
120+
{ fields: ['revoked'] },
94121
],
95-
122+
96123
enable: {
97124
trackHistory: true,
98125
searchable: false,

0 commit comments

Comments
 (0)