portal-mojo 0.2.7 → 0.2.8

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.
@@ -0,0 +1,153 @@
1
+ // account/credential-models — the caller-owned credential models the admin
2
+ // User detail needs EAGERLY (PasskeyModel, ApiKeyModel + useGenerateUserApiKey,
3
+ // OAuthConnectionModel). Split out of account/models so the admin graph never
4
+ // pulls the account wire (account/api) or the rest of the module into its
5
+ // eager chunk: this file imports NOTHING else from portal-mojo/account.
6
+ // account/models and admin/identity/users/models re-export these unchanged —
7
+ // ONE defineModel per endpoint, so both surfaces share the cache keys.
8
+ import { useMutation, useQueryClient } from '@tanstack/react-query';
9
+ import { defineModel, mojoCall, withFreshAuth } from '../client/runtime';
10
+
11
+ /** The global user-administration gate (admin's USER_MANAGE_PERMISSIONS). */
12
+ const USER_ADMIN_PERMISSIONS = ['sys.users', 'sys.manage_users'];
13
+
14
+ // ── Passkeys ──────────────────────────────────────────────────────────
15
+
16
+ export interface PasskeyRow {
17
+ id: number;
18
+ friendly_name: string | null;
19
+ credential_id: string;
20
+ rp_id: string;
21
+ is_enabled: boolean;
22
+ sign_count: number;
23
+ transports: string | null;
24
+ aaguid: string | null;
25
+ last_used: number | null;
26
+ created: number;
27
+ }
28
+
29
+ /**
30
+ * `/api/account/passkeys`. Always list with `?user=<id>`: a `users` /
31
+ * `manage_users` caller is otherwise served EVERY user's passkeys
32
+ * (django-mojo models/rest.py:927-954). Editable: friendly_name, is_enabled.
33
+ */
34
+ export const PasskeyModel = defineModel<PasskeyRow>({
35
+ name: 'passkey', endpoint: '/api/account/passkeys',
36
+ permissions: { view: USER_ADMIN_PERMISSIONS, manage: USER_ADMIN_PERMISSIONS },
37
+ });
38
+
39
+ // ── API keys ──────────────────────────────────────────────────────────
40
+
41
+ /** `/api/account/api_keys` never serializes its owner or signing material. */
42
+ export interface ApiKeyRow {
43
+ id: number;
44
+ label: string;
45
+ allowed_ips: string[];
46
+ expires: number;
47
+ is_active: boolean;
48
+ last_used: number | null;
49
+ created: number;
50
+ }
51
+
52
+ function sanitizeApiKeyRow(row: ApiKeyRow): ApiKeyRow {
53
+ const safe = { ...row } as ApiKeyRow & Record<string, unknown>;
54
+ for (const key of ['token', 'jti', 'auth_key', 'secret', 'token_hash']) delete safe[key];
55
+ return safe;
56
+ }
57
+
58
+ export const ApiKeyModel = defineModel<ApiKeyRow>({
59
+ name: 'api_key',
60
+ endpoint: '/api/account/api_keys',
61
+ permissions: { manageOthers: USER_ADMIN_PERMISSIONS },
62
+ sanitizeRow: sanitizeApiKeyRow,
63
+ forms: {
64
+ generate: {
65
+ title: 'Generate API key',
66
+ submitText: 'Generate',
67
+ fields: [
68
+ { name: 'label', type: 'text', label: 'Label', required: true, placeholder: 'e.g. CI/CD pipeline, Mobile app' },
69
+ {
70
+ name: 'allowed_ips', type: 'text', label: 'Allowed IPs',
71
+ placeholder: 'e.g. 203.0.113.0/24, 10.0.0.1',
72
+ help: 'Optional. Comma-separated IPs or CIDR ranges; empty allows any.',
73
+ },
74
+ {
75
+ name: 'expire_days', type: 'select', label: 'Expires in', options: [
76
+ { value: '30', label: '30 days' },
77
+ { value: '60', label: '60 days' },
78
+ { value: '90', label: '90 days' },
79
+ { value: '180', label: '180 days' },
80
+ { value: '360', label: '360 days (max)' },
81
+ ],
82
+ },
83
+ ],
84
+ },
85
+ },
86
+ actions: { revoke: { response: 'payload' } },
87
+ });
88
+
89
+ export interface GenerateUserApiKeyVariables {
90
+ changes: Record<string, unknown>;
91
+ /** Receives the token while the mutation is still pending. */
92
+ onToken?: (token: string) => void | Promise<void>;
93
+ }
94
+
95
+ interface GeneratedKeyResponse {
96
+ id?: number;
97
+ expires?: number;
98
+ token?: string;
99
+ jti?: string;
100
+ auth_key?: string;
101
+ secret?: string;
102
+ }
103
+
104
+ export interface GeneratedKeyReceipt {
105
+ id: number | null;
106
+ expires: number | null;
107
+ }
108
+
109
+ /**
110
+ * Caller-only key generation. The raw response is split before mutation
111
+ * resolution, so TanStack MutationCache receives only non-secret metadata.
112
+ */
113
+ export function useGenerateUserApiKey() {
114
+ const qc = useQueryClient();
115
+ return useMutation<GeneratedKeyReceipt, Error, GenerateUserApiKeyVariables>({
116
+ mutationFn: async ({ changes, onToken }) => {
117
+ const response = await withFreshAuth(() => mojoCall('/api/auth/generate_api_key', {
118
+ method: 'POST',
119
+ body: changes,
120
+ }));
121
+ const payload = response.data as GeneratedKeyResponse | undefined;
122
+ const created: GeneratedKeyResponse = payload && typeof payload === 'object' ? { ...payload } : {};
123
+ let token = typeof created.token === 'string' && created.token ? created.token : null;
124
+ try {
125
+ if (token && onToken) await onToken(token);
126
+ } finally {
127
+ token = null;
128
+ for (const key of ['token', 'jti', 'auth_key', 'secret'] as const) delete created[key];
129
+ }
130
+ return {
131
+ id: typeof created.id === 'number' ? created.id : null,
132
+ expires: typeof created.expires === 'number' ? created.expires : null,
133
+ };
134
+ },
135
+ onSuccess: () => { void qc.invalidateQueries({ queryKey: ApiKeyModel.keys.root }); },
136
+ });
137
+ }
138
+
139
+ // ── OAuth connections ─────────────────────────────────────────────────
140
+
141
+ export interface OAuthConnectionRow {
142
+ id: number;
143
+ provider: string;
144
+ email: string | null;
145
+ is_active: boolean;
146
+ created: number;
147
+ }
148
+
149
+ /** `/api/account/oauth_connection` — DELETE unlinks; the server refuses the last login method (400). */
150
+ export const OAuthConnectionModel = defineModel<OAuthConnectionRow>({
151
+ name: 'oauth_connection', endpoint: '/api/account/oauth_connection',
152
+ permissions: { view: USER_ADMIN_PERMISSIONS, manage: USER_ADMIN_PERMISSIONS },
153
+ });