create-aphex 0.1.3 → 0.2.1

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.
Files changed (104) hide show
  1. package/dist/index.js +9 -88
  2. package/package.json +4 -4
  3. package/templates/base/.dockerignore +0 -44
  4. package/templates/base/.env.example +0 -38
  5. package/templates/base/CHANGELOG.md +0 -124
  6. package/templates/base/Dockerfile +0 -69
  7. package/templates/base/README.md +0 -147
  8. package/templates/base/aphex.config.ts +0 -45
  9. package/templates/base/docker-compose.yml +0 -31
  10. package/templates/base/drizzle/0000_tiny_redwing.sql +0 -226
  11. package/templates/base/drizzle/meta/0000_snapshot.json +0 -1379
  12. package/templates/base/drizzle/meta/_journal.json +0 -13
  13. package/templates/base/drizzle.config.ts +0 -17
  14. package/templates/base/package.json +0 -81
  15. package/templates/base/prod.docker-compose.yml +0 -61
  16. package/templates/base/src/app.css +0 -32
  17. package/templates/base/src/app.d.ts +0 -26
  18. package/templates/base/src/app.html +0 -11
  19. package/templates/base/src/hooks.server.ts +0 -52
  20. package/templates/base/src/lib/assets/favicon.svg +0 -5
  21. package/templates/base/src/lib/auth-client.ts +0 -19
  22. package/templates/base/src/lib/index.ts +0 -1
  23. package/templates/base/src/lib/schemaTypes/index.ts +0 -11
  24. package/templates/base/src/lib/schemaTypes/post.ts +0 -47
  25. package/templates/base/src/lib/server/auth/better-auth/instance.ts +0 -183
  26. package/templates/base/src/lib/server/auth/index.ts +0 -25
  27. package/templates/base/src/lib/server/auth/instance.ts +0 -7
  28. package/templates/base/src/lib/server/auth/service.ts +0 -486
  29. package/templates/base/src/lib/server/cache/index.ts +0 -10
  30. package/templates/base/src/lib/server/db/auth-schema.ts +0 -86
  31. package/templates/base/src/lib/server/db/cms-schema.ts +0 -23
  32. package/templates/base/src/lib/server/db/index.ts +0 -55
  33. package/templates/base/src/lib/server/db/schema.ts +0 -3
  34. package/templates/base/src/lib/server/email/components/EmailLayout.svelte +0 -24
  35. package/templates/base/src/lib/server/email/components/EmailVerification.svelte +0 -26
  36. package/templates/base/src/lib/server/email/components/Invitation.svelte +0 -30
  37. package/templates/base/src/lib/server/email/components/PasswordReset.svelte +0 -28
  38. package/templates/base/src/lib/server/email/index.ts +0 -28
  39. package/templates/base/src/lib/server/email/invitation-hook.ts +0 -56
  40. package/templates/base/src/lib/server/email/renderer.ts +0 -13
  41. package/templates/base/src/lib/server/email/templates/email-verification.ts +0 -8
  42. package/templates/base/src/lib/server/email/templates/invitation.ts +0 -8
  43. package/templates/base/src/lib/server/email/templates/password-reset.ts +0 -7
  44. package/templates/base/src/lib/server/services/organization.ts +0 -68
  45. package/templates/base/src/lib/server/storage/index.ts +0 -28
  46. package/templates/base/src/lib/stores/activeTab.svelte.ts +0 -14
  47. package/templates/base/src/lib/utils/imageBuilder.ts +0 -3
  48. package/templates/base/src/lib/utils/slug.ts +0 -33
  49. package/templates/base/src/routes/(protected)/admin/+layout.server.ts +0 -86
  50. package/templates/base/src/routes/(protected)/admin/+layout.svelte +0 -78
  51. package/templates/base/src/routes/(protected)/admin/+page.server.ts +0 -60
  52. package/templates/base/src/routes/(protected)/admin/+page.svelte +0 -34
  53. package/templates/base/src/routes/(protected)/admin/organizations/+page.server.ts +0 -39
  54. package/templates/base/src/routes/(protected)/admin/organizations/+page.svelte +0 -31
  55. package/templates/base/src/routes/(protected)/admin/organizations/_components/CreateOrganization.svelte +0 -141
  56. package/templates/base/src/routes/(protected)/admin/organizations/_components/OrganizationsList.svelte +0 -102
  57. package/templates/base/src/routes/(protected)/admin/settings/+layout.server.ts +0 -42
  58. package/templates/base/src/routes/(protected)/admin/settings/+layout.svelte +0 -116
  59. package/templates/base/src/routes/(protected)/admin/settings/+page.server.ts +0 -16
  60. package/templates/base/src/routes/(protected)/admin/settings/+page.svelte +0 -32
  61. package/templates/base/src/routes/(protected)/admin/settings/_components/AccountSettings.svelte +0 -177
  62. package/templates/base/src/routes/(protected)/admin/settings/_components/ApiKeysSettings.svelte +0 -472
  63. package/templates/base/src/routes/(protected)/admin/settings/_components/OrganizationsSettings.svelte +0 -182
  64. package/templates/base/src/routes/(protected)/admin/settings/account/+page.server.ts +0 -25
  65. package/templates/base/src/routes/(protected)/admin/settings/account/+page.svelte +0 -23
  66. package/templates/base/src/routes/(protected)/admin/settings/api-keys/+page.server.ts +0 -54
  67. package/templates/base/src/routes/(protected)/admin/settings/api-keys/+page.svelte +0 -14
  68. package/templates/base/src/routes/(protected)/admin/settings/members/+page.server.ts +0 -31
  69. package/templates/base/src/routes/(protected)/admin/settings/members/+page.svelte +0 -338
  70. package/templates/base/src/routes/(protected)/admin/settings/roles/+page.server.ts +0 -17
  71. package/templates/base/src/routes/(protected)/admin/settings/roles/+page.svelte +0 -339
  72. package/templates/base/src/routes/+layout.svelte +0 -12
  73. package/templates/base/src/routes/+page.svelte +0 -2
  74. package/templates/base/src/routes/api/[...slug]/+server.ts +0 -35
  75. package/templates/base/src/routes/api/instance-settings/+server.ts +0 -70
  76. package/templates/base/src/routes/api/invitations/+server.ts +0 -54
  77. package/templates/base/src/routes/api/invitations/[id]/accept/+server.ts +0 -54
  78. package/templates/base/src/routes/api/invitations/[id]/reject/+server.ts +0 -49
  79. package/templates/base/src/routes/api/settings/api-keys/+server.ts +0 -57
  80. package/templates/base/src/routes/api/settings/api-keys/[id]/+server.ts +0 -53
  81. package/templates/base/src/routes/god-mode/+layout.server.ts +0 -32
  82. package/templates/base/src/routes/god-mode/+layout.svelte +0 -155
  83. package/templates/base/src/routes/god-mode/+page.server.ts +0 -10
  84. package/templates/base/src/routes/god-mode/+page.svelte +0 -32
  85. package/templates/base/src/routes/god-mode/_components/CreateOrganization.svelte +0 -141
  86. package/templates/base/src/routes/god-mode/_components/OrganizationsList.svelte +0 -169
  87. package/templates/base/src/routes/god-mode/organizations/+page.server.ts +0 -43
  88. package/templates/base/src/routes/god-mode/organizations/+page.svelte +0 -74
  89. package/templates/base/src/routes/invitations/+page.server.ts +0 -48
  90. package/templates/base/src/routes/invitations/+page.svelte +0 -264
  91. package/templates/base/src/routes/invite/[token]/+page.server.ts +0 -59
  92. package/templates/base/src/routes/invite/[token]/+page.svelte +0 -109
  93. package/templates/base/src/routes/login/+page.server.ts +0 -14
  94. package/templates/base/src/routes/login/+page.svelte +0 -332
  95. package/templates/base/src/routes/media/[id]/[filename]/+server.ts +0 -3
  96. package/templates/base/src/routes/reset-password/[token]/+page.server.ts +0 -15
  97. package/templates/base/src/routes/reset-password/[token]/+page.svelte +0 -179
  98. package/templates/base/src/routes/verify-email/+page.svelte +0 -41
  99. package/templates/base/static/images/aphex-darkmode.png +0 -0
  100. package/templates/base/static/images/aphex-lightmode.png +0 -0
  101. package/templates/base/static/robots.txt +0 -3
  102. package/templates/base/svelte.config.js +0 -19
  103. package/templates/base/tsconfig.json +0 -20
  104. package/templates/base/vite.config.ts +0 -146
@@ -1,472 +0,0 @@
1
- <script lang="ts">
2
- import { Button } from '@aphexcms/ui/shadcn/button';
3
- import { Input } from '@aphexcms/ui/shadcn/input';
4
- import { Label } from '@aphexcms/ui/shadcn/label';
5
- import { Badge } from '@aphexcms/ui/shadcn/badge';
6
- import {
7
- Dialog,
8
- DialogContent,
9
- DialogDescription,
10
- DialogFooter,
11
- DialogHeader,
12
- DialogTitle,
13
- DialogTrigger
14
- } from '@aphexcms/ui/shadcn/dialog';
15
- import * as Select from '@aphexcms/ui/shadcn/select';
16
- import * as Card from '@aphexcms/ui/shadcn/card';
17
- import * as Tooltip from '@aphexcms/ui/shadcn/tooltip';
18
- import * as Collapsible from '@aphexcms/ui/shadcn/collapsible';
19
- import { Separator } from '@aphexcms/ui/shadcn/separator';
20
- import { apiKeys as apiKeysApi } from '@aphexcms/cms-core/client';
21
- import { confirmDialog, usePermissions } from '@aphexcms/cms-core/client';
22
- import { invalidateAll } from '$app/navigation';
23
- import { toast } from 'svelte-sonner';
24
- import { KeyRound, Copy, Trash2, Plus, ChevronDown } from '@lucide/svelte';
25
-
26
- type ApiKey = {
27
- id: string;
28
- name: string | null;
29
- permissions: ('read' | 'write')[];
30
- createdAt: Date | null;
31
- lastRequest: Date | null;
32
- expiresAt: Date | null;
33
- };
34
-
35
- type Props = {
36
- apiKeys: ApiKey[];
37
- organizationRole?: string | null;
38
- };
39
-
40
- let { apiKeys, organizationRole }: Props = $props();
41
-
42
- // Capability-driven gate — any role (built-in or custom) with apiKey.manage
43
- // gets the create/delete affordances. The legacy `organizationRole` prop is
44
- // kept in the signature for backwards compat but no longer drives the UI.
45
- void organizationRole;
46
- const perms = usePermissions();
47
- const canManageApiKeys = $derived(perms.can('apiKey.manage'));
48
-
49
- let createDialogOpen = $state(false);
50
- let newKeyName = $state('');
51
- let newKeyMode = $state<'read' | 'write'>('read');
52
- const newKeyPermissions = $derived<('read' | 'write')[]>(
53
- newKeyMode === 'write' ? ['read', 'write'] : ['read']
54
- );
55
- let newKeyExpiresValue = $state('365');
56
- let newKeyExpiresInDays = $state<number | undefined>(365);
57
- let createdKey = $state<{ key: string; name: string } | null>(null);
58
- let isCreating = $state(false);
59
-
60
- const expirationOptions = [
61
- { value: '30', label: '30 days' },
62
- { value: '90', label: '90 days' },
63
- { value: '365', label: '1 year' },
64
- { value: 'never', label: 'Never' }
65
- ];
66
-
67
- const expirationTriggerContent = $derived(
68
- expirationOptions.find((opt) => opt.value === newKeyExpiresValue)?.label ?? '1 year'
69
- );
70
-
71
- async function createApiKey() {
72
- if (!newKeyName.trim()) {
73
- toast.error('Please enter a key name');
74
- return;
75
- }
76
-
77
- isCreating = true;
78
- try {
79
- const result = await apiKeysApi.create({
80
- name: newKeyName.trim(),
81
- permissions: newKeyPermissions,
82
- expiresInDays: newKeyExpiresInDays
83
- });
84
-
85
- if (!result.success || !result.data) {
86
- throw new Error(result.error || 'Failed to create API key');
87
- }
88
-
89
- createdKey = { key: result.data.apiKey.key, name: result.data.apiKey.name ?? newKeyName };
90
-
91
- // Reset form
92
- newKeyName = '';
93
- newKeyMode = 'read';
94
- newKeyExpiresValue = '365';
95
- newKeyExpiresInDays = 365;
96
-
97
- // Refresh the list
98
- await invalidateAll();
99
- } catch (error) {
100
- toast.error(error instanceof Error ? error.message : 'Failed to create API key');
101
- } finally {
102
- isCreating = false;
103
- }
104
- }
105
-
106
- async function deleteApiKey(id: string, name: string) {
107
- const confirmed = await confirmDialog({
108
- title: `Delete "${name}"?`,
109
- description: 'This action cannot be undone.',
110
- confirmText: 'Delete',
111
- variant: 'destructive'
112
- });
113
- if (!confirmed) return;
114
-
115
- try {
116
- const result = await apiKeysApi.remove(id);
117
-
118
- if (!result.success) {
119
- throw new Error(result.error || 'Failed to delete API key');
120
- }
121
-
122
- toast.success(`API key "${name}" deleted`);
123
- await invalidateAll();
124
- } catch (error) {
125
- toast.error(error instanceof Error ? error.message : 'Failed to delete API key');
126
- }
127
- }
128
-
129
- function copyToClipboard(text: string) {
130
- navigator.clipboard.writeText(text);
131
- toast.success('Copied to clipboard!');
132
- }
133
-
134
- function formatDate(date: Date | null | undefined) {
135
- if (!date) return 'Never';
136
- return new Date(date).toLocaleDateString('en-US', {
137
- year: 'numeric',
138
- month: 'short',
139
- day: 'numeric'
140
- });
141
- }
142
- </script>
143
-
144
- <Card.Root>
145
- <Card.Header>
146
- <div class="flex items-center justify-between">
147
- <div>
148
- <Card.Title>API Keys</Card.Title>
149
- <Card.Description>API keys allow programmatic access to your CMS content</Card.Description>
150
- </div>
151
- {#if canManageApiKeys}
152
- <Dialog bind:open={createDialogOpen}>
153
- {#if apiKeys.length > 0}
154
- <!-- Only show the top-right trigger when there's already a list.
155
- The empty state renders its own inline Create button that
156
- flips `createDialogOpen` directly — showing both CTAs on an
157
- empty screen makes the layout feel cluttered. -->
158
- <DialogTrigger>
159
- {#snippet child({ props })}
160
- <Button size="sm" {...props}>
161
- <Plus class="mr-1.5 h-4 w-4" />
162
- Create Key
163
- </Button>
164
- {/snippet}
165
- </DialogTrigger>
166
- {/if}
167
- <DialogContent class="sm:max-w-[500px]">
168
- {#if createdKey}
169
- <DialogHeader>
170
- <DialogTitle>API Key Created</DialogTitle>
171
- <DialogDescription>
172
- Save this key securely - you won't be able to see it again
173
- </DialogDescription>
174
- </DialogHeader>
175
- <div class="space-y-4 py-4">
176
- <div>
177
- <Label>Key Name</Label>
178
- <p class="mt-1 text-sm font-medium">{createdKey.name}</p>
179
- </div>
180
- <div>
181
- <Label>API Key</Label>
182
- <div class="mt-1 flex gap-2">
183
- <Input value={createdKey.key} readonly class="font-mono text-xs" />
184
- <Button
185
- size="sm"
186
- variant="outline"
187
- onclick={() => copyToClipboard(createdKey!.key)}
188
- >
189
- Copy
190
- </Button>
191
- </div>
192
- </div>
193
- </div>
194
- <DialogFooter>
195
- <Button
196
- onclick={() => {
197
- createdKey = null;
198
- createDialogOpen = false;
199
- }}
200
- >
201
- Done
202
- </Button>
203
- </DialogFooter>
204
- {:else}
205
- <DialogHeader>
206
- <DialogTitle>Create API Key</DialogTitle>
207
- <DialogDescription>
208
- Generate a new API key for programmatic access
209
- </DialogDescription>
210
- </DialogHeader>
211
- <div class="space-y-4 py-4">
212
- <div>
213
- <Label for="key-name">Key Name</Label>
214
- <Input
215
- id="key-name"
216
- bind:value={newKeyName}
217
- placeholder="Production API Key"
218
- class="mt-1"
219
- />
220
- <p class="text-muted-foreground mt-1 text-xs">
221
- A descriptive name to identify this key
222
- </p>
223
- </div>
224
-
225
- <div>
226
- <Label>Access Level</Label>
227
- <div class="mt-2 flex gap-2">
228
- <Button
229
- variant={newKeyMode === 'read' ? 'default' : 'outline'}
230
- size="sm"
231
- onclick={() => (newKeyMode = 'read')}
232
- >
233
- Read only
234
- </Button>
235
- <Button
236
- variant={newKeyMode === 'write' ? 'default' : 'outline'}
237
- size="sm"
238
- onclick={() => (newKeyMode = 'write')}
239
- >
240
- Read + Write
241
- </Button>
242
- </div>
243
- </div>
244
-
245
- <div>
246
- <Label for="expires">Expires In</Label>
247
- <Select.Root
248
- type="single"
249
- name="expiration"
250
- bind:value={newKeyExpiresValue}
251
- onValueChange={(value) => {
252
- if (value) {
253
- newKeyExpiresInDays = value === 'never' ? undefined : parseInt(value);
254
- }
255
- }}
256
- >
257
- <Select.Trigger class="mt-1 w-[180px]">
258
- {expirationTriggerContent}
259
- </Select.Trigger>
260
- <Select.Content>
261
- <Select.Group>
262
- {#each expirationOptions as option (option.value)}
263
- <Select.Item value={option.value} label={option.label}>
264
- {option.label}
265
- </Select.Item>
266
- {/each}
267
- </Select.Group>
268
- </Select.Content>
269
- </Select.Root>
270
- </div>
271
- </div>
272
- <DialogFooter>
273
- <Button
274
- variant="outline"
275
- onclick={() => (createDialogOpen = false)}
276
- disabled={isCreating}
277
- >
278
- Cancel
279
- </Button>
280
- <Button onclick={createApiKey} disabled={isCreating}>
281
- {isCreating ? 'Creating...' : 'Create Key'}
282
- </Button>
283
- </DialogFooter>
284
- {/if}
285
- </DialogContent>
286
- </Dialog>
287
- {/if}
288
- </div>
289
- </Card.Header>
290
-
291
- <Card.Content>
292
- {#if apiKeys.length === 0}
293
- <div class="flex flex-col items-center justify-center py-12 text-center">
294
- <div class="bg-muted mb-4 rounded-full p-3">
295
- <KeyRound class="text-muted-foreground h-6 w-6" />
296
- </div>
297
- <p class="text-base font-medium">No API keys yet</p>
298
- <p class="text-muted-foreground mt-1 text-sm">
299
- Create your first API key to access the CMS data programmatically
300
- </p>
301
- {#if canManageApiKeys}
302
- <Button size="sm" class="mt-4" onclick={() => (createDialogOpen = true)}>
303
- <Plus class="mr-1.5 h-4 w-4" />
304
- Create API Key
305
- </Button>
306
- {/if}
307
- </div>
308
- {:else}
309
- <div class="divide-y">
310
- {#each apiKeys as apiKey, i (apiKey.id)}
311
- <div
312
- class="flex items-center justify-between gap-4 {i > 0 ? 'pt-4' : ''} {i <
313
- apiKeys.length - 1
314
- ? 'pb-4'
315
- : ''}"
316
- >
317
- <div class="min-w-0 flex-1">
318
- <div class="flex items-center gap-2">
319
- <KeyRound class="text-muted-foreground h-4 w-4 shrink-0" />
320
- <span class="truncate font-medium">{apiKey.name}</span>
321
- <div class="flex gap-1">
322
- {#each apiKey.permissions as permission}
323
- <Badge variant="secondary" class="text-xs capitalize">
324
- {permission}
325
- </Badge>
326
- {/each}
327
- </div>
328
- </div>
329
- <div class="text-muted-foreground mt-1.5 ml-6 flex flex-wrap gap-x-4 gap-y-1 text-xs">
330
- <span>Created {formatDate(apiKey.createdAt)}</span>
331
- <span>Last used {formatDate(apiKey.lastRequest)}</span>
332
- <span>
333
- Expires {apiKey.expiresAt ? formatDate(apiKey.expiresAt) : 'never'}
334
- </span>
335
- </div>
336
- </div>
337
- {#if canManageApiKeys}
338
- <Tooltip.Root>
339
- <Tooltip.Trigger>
340
- {#snippet child({ props })}
341
- <Button
342
- {...props}
343
- variant="ghost"
344
- size="icon"
345
- class="text-muted-foreground hover:text-destructive h-8 w-8 shrink-0"
346
- onclick={(e) => {
347
- props.onclick?.(e);
348
- deleteApiKey(apiKey.id, apiKey.name ?? 'Unnamed');
349
- }}
350
- >
351
- <Trash2 class="h-4 w-4" />
352
- </Button>
353
- {/snippet}
354
- </Tooltip.Trigger>
355
- <Tooltip.Content>Delete this API key</Tooltip.Content>
356
- </Tooltip.Root>
357
- {/if}
358
- </div>
359
- {/each}
360
- </div>
361
- {/if}
362
- </Card.Content>
363
- </Card.Root>
364
-
365
- <!-- Quick Reference -->
366
- <Card.Root class="mt-6">
367
- <Card.Header class="pb-3">
368
- <Card.Title class="text-sm font-medium">Quick Reference</Card.Title>
369
- <Card.Description>
370
- Pass your key via the <code class="bg-muted rounded px-1 py-0.5 text-xs">x-api-key</code> header
371
- </Card.Description>
372
- </Card.Header>
373
- <Card.Content class="space-y-4">
374
- <!-- Auth example -->
375
- <div class="bg-muted relative rounded-md p-3">
376
- <code class="block font-mono text-xs leading-relaxed break-all">
377
- curl -H "x-api-key: your_key_here" \<br />
378
- &nbsp;&nbsp;https://your-app.com/api/documents?type=&#123;schemaType&#125;
379
- </code>
380
- <Button
381
- variant="ghost"
382
- size="icon"
383
- class="absolute top-2 right-2 h-7 w-7"
384
- onclick={() =>
385
- copyToClipboard(
386
- 'curl -H "x-api-key: your_key_here" \\\n https://your-app.com/api/documents?type={schemaType}'
387
- )}
388
- >
389
- <Copy class="h-3.5 w-3.5" />
390
- </Button>
391
- </div>
392
-
393
- <!-- Endpoints -->
394
- <Collapsible.Root>
395
- <Collapsible.Trigger
396
- class="flex w-full items-center justify-between text-xs font-medium [&[data-state=open]>svg]:rotate-180"
397
- >
398
- Endpoints
399
- <ChevronDown class="text-muted-foreground h-3.5 w-3.5 transition-transform duration-200" />
400
- </Collapsible.Trigger>
401
- <Collapsible.Content>
402
- <div class="bg-muted mt-2 overflow-hidden rounded-md font-mono text-xs">
403
- <div class="divide-border divide-y">
404
- <div class="flex gap-2 px-3 py-1.5">
405
- <Badge variant="outline" class="w-14 justify-center font-mono text-[10px]">GET</Badge>
406
- <span class="text-muted-foreground">/api/documents?type=&#123;type&#125;</span>
407
- </div>
408
- <div class="flex gap-2 px-3 py-1.5">
409
- <Badge variant="outline" class="w-14 justify-center font-mono text-[10px]">GET</Badge>
410
- <span class="text-muted-foreground">/api/documents/&#123;id&#125;</span>
411
- </div>
412
- <div class="flex gap-2 px-3 py-1.5">
413
- <Badge variant="secondary" class="w-14 justify-center font-mono text-[10px]"
414
- >POST</Badge
415
- >
416
- <span class="text-muted-foreground">/api/documents</span>
417
- </div>
418
- <div class="flex gap-2 px-3 py-1.5">
419
- <Badge variant="secondary" class="w-14 justify-center font-mono text-[10px]"
420
- >PUT</Badge
421
- >
422
- <span class="text-muted-foreground">/api/documents/&#123;id&#125;</span>
423
- </div>
424
- <div class="flex gap-2 px-3 py-1.5">
425
- <Badge variant="secondary" class="w-14 justify-center font-mono text-[10px]"
426
- >DEL</Badge
427
- >
428
- <span class="text-muted-foreground">/api/documents/&#123;id&#125;</span>
429
- </div>
430
- <div class="flex gap-2 px-3 py-1.5">
431
- <Badge variant="outline" class="w-14 justify-center font-mono text-[10px]">POST</Badge
432
- >
433
- <span class="text-muted-foreground">/api/documents/query</span>
434
- </div>
435
- <div class="flex gap-2 px-3 py-1.5">
436
- <Badge variant="outline" class="w-14 justify-center font-mono text-[10px]">GET</Badge>
437
- <span class="text-muted-foreground">/api/assets</span>
438
- </div>
439
- <div class="flex gap-2 px-3 py-1.5">
440
- <Badge variant="secondary" class="w-14 justify-center font-mono text-[10px]"
441
- >POST</Badge
442
- >
443
- <span class="text-muted-foreground">/api/assets</span>
444
- <span class="text-muted-foreground/60 ml-auto text-[10px]">multipart/form-data</span>
445
- </div>
446
- <div class="flex gap-2 px-3 py-1.5">
447
- <Badge variant="outline" class="w-14 justify-center font-mono text-[10px]">GET</Badge>
448
- <span class="text-muted-foreground">/api/schemas</span>
449
- </div>
450
- </div>
451
- </div>
452
- </Collapsible.Content>
453
- </Collapsible.Root>
454
-
455
- <!-- Permissions note -->
456
- <div class="text-muted-foreground text-xs leading-relaxed">
457
- <p>
458
- <Badge variant="outline" class="mr-1 font-mono text-[10px]">GET</Badge> endpoints need
459
- <strong class="text-foreground">read</strong> permission.
460
- <Badge variant="secondary" class="mr-1 ml-1 font-mono text-[10px]">POST</Badge>
461
- <Badge variant="secondary" class="mr-1 font-mono text-[10px]">PUT</Badge>
462
- <Badge variant="secondary" class="mr-1 font-mono text-[10px]">DEL</Badge> need
463
- <strong class="text-foreground">write</strong>. Read-only keys get
464
- <code class="bg-muted rounded px-1 py-0.5">403</code> on mutations.
465
- </p>
466
- <p class="mt-1">
467
- <code class="bg-muted rounded px-1 py-0.5">POST /api/documents/query</code> is the exception
468
- — it only needs <strong class="text-foreground">read</strong> permission.
469
- </p>
470
- </div>
471
- </Card.Content>
472
- </Card.Root>
@@ -1,182 +0,0 @@
1
- <script lang="ts">
2
- import { Button } from '@aphexcms/ui/shadcn/button';
3
- import { Input } from '@aphexcms/ui/shadcn/input';
4
- import { Label } from '@aphexcms/ui/shadcn/label';
5
- import * as Card from '@aphexcms/ui/shadcn/card';
6
- import * as Avatar from '@aphexcms/ui/shadcn/avatar';
7
- import { Separator } from '@aphexcms/ui/shadcn/separator';
8
- import { invalidateAll } from '$app/navigation';
9
- import { toast } from 'svelte-sonner';
10
- import { Copy, Users } from '@lucide/svelte';
11
- import type { Organization } from '@aphexcms/cms-core';
12
- import { organizations } from '@aphexcms/cms-core/client';
13
-
14
- type Props = {
15
- activeOrganization: Organization & { members: any[] };
16
- currentUserId: string;
17
- };
18
-
19
- let { activeOrganization, currentUserId }: Props = $props();
20
-
21
- let editOrgName = $state(activeOrganization.name);
22
- let editOrgSlug = $state(activeOrganization.slug);
23
- let isUpdatingOrg = $state(false);
24
- let error = $state<string | null>(null);
25
-
26
- // Sync local state when the active organization changes (e.g. org switch)
27
- $effect(() => {
28
- editOrgName = activeOrganization.name;
29
- editOrgSlug = activeOrganization.slug;
30
- error = null;
31
- });
32
-
33
- const orgInitials = $derived(
34
- activeOrganization.name
35
- .split(' ')
36
- .map((w) => w[0])
37
- .join('')
38
- .toUpperCase()
39
- .slice(0, 2)
40
- );
41
-
42
- const formattedDate = $derived(
43
- new Date(activeOrganization.createdAt).toLocaleDateString('en-US', {
44
- year: 'numeric',
45
- month: 'long',
46
- day: 'numeric'
47
- })
48
- );
49
-
50
- function generateSlug(text: string): string {
51
- return text
52
- .toLowerCase()
53
- .trim()
54
- .replace(/[^\w\s-]/g, '')
55
- .replace(/[\s_-]+/g, '-')
56
- .replace(/^-+|-+$/g, '');
57
- }
58
-
59
- function handleSlugInput(event: Event) {
60
- const target = event.target as HTMLInputElement;
61
- editOrgSlug = target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
62
- }
63
-
64
- function copyToClipboard(text: string) {
65
- navigator.clipboard.writeText(text);
66
- toast.success('Copied to clipboard!');
67
- }
68
-
69
- async function updateOrganization() {
70
- if (!editOrgName.trim() || !editOrgSlug.trim()) {
71
- error = 'Please enter both organization name and slug';
72
- return;
73
- }
74
-
75
- isUpdatingOrg = true;
76
- error = null;
77
- try {
78
- const result = await organizations.update(activeOrganization.id, {
79
- name: editOrgName.trim(),
80
- slug: editOrgSlug.trim()
81
- });
82
-
83
- if (!result.success) {
84
- throw new Error(result.error || 'Failed to update organization');
85
- }
86
-
87
- toast.success('Organization updated successfully');
88
- await invalidateAll();
89
- } catch (err) {
90
- error = err instanceof Error ? err.message : 'Failed to update organization';
91
- } finally {
92
- isUpdatingOrg = false;
93
- }
94
- }
95
- </script>
96
-
97
- <Card.Root>
98
- <Card.Header>
99
- <div class="flex items-center gap-4">
100
- <Avatar.Root class="h-12 w-12 text-lg">
101
- {#if activeOrganization.metadata?.logo}
102
- <Avatar.Image src={activeOrganization.metadata.logo} alt={activeOrganization.name} />
103
- {/if}
104
- <Avatar.Fallback>{orgInitials}</Avatar.Fallback>
105
- </Avatar.Root>
106
- <div class="min-w-0 flex-1">
107
- <Card.Title class="text-lg">{activeOrganization.name}</Card.Title>
108
- <div class="text-muted-foreground mt-1 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs">
109
- <span class="flex items-center gap-1">
110
- <Users class="h-3 w-3" />
111
- {activeOrganization.members.length} member{activeOrganization.members.length !== 1
112
- ? 's'
113
- : ''}
114
- </span>
115
- <span>Created {formattedDate}</span>
116
- </div>
117
- </div>
118
- </div>
119
- </Card.Header>
120
-
121
- <Card.Content>
122
- <!-- Org ID -->
123
- <div class="mb-4">
124
- <Label class="text-muted-foreground text-xs">Organization ID</Label>
125
- <div class="mt-1 flex items-center gap-2">
126
- <code
127
- class="bg-muted text-muted-foreground flex-1 truncate rounded-md px-3 py-1.5 font-mono text-xs"
128
- >
129
- {activeOrganization.id}
130
- </code>
131
- <Button
132
- variant="ghost"
133
- size="icon"
134
- class="h-7 w-7 shrink-0"
135
- onclick={() => copyToClipboard(activeOrganization.id)}
136
- >
137
- <Copy class="h-3.5 w-3.5" />
138
- </Button>
139
- </div>
140
- </div>
141
-
142
- <Separator class="mb-4" />
143
-
144
- <div class="grid gap-4">
145
- <div>
146
- <Label for="org-name">Organization Name</Label>
147
- <Input id="org-name" bind:value={editOrgName} placeholder="Acme Inc" class="mt-2" />
148
- </div>
149
- <div>
150
- <Label for="org-slug">Slug</Label>
151
- <div class="mt-2 flex gap-2">
152
- <Input
153
- id="org-slug"
154
- value={editOrgSlug}
155
- oninput={handleSlugInput}
156
- placeholder="acme-inc"
157
- class="flex-1"
158
- />
159
- <Button
160
- variant="outline"
161
- size="sm"
162
- onclick={() => (editOrgSlug = generateSlug(editOrgName))}
163
- disabled={!editOrgName.trim()}
164
- >
165
- Generate
166
- </Button>
167
- </div>
168
- <p class="text-muted-foreground mt-1.5 font-mono text-xs">
169
- /{editOrgSlug || 'your-slug'}
170
- </p>
171
- </div>
172
- {#if error}
173
- <p class="text-destructive text-sm">{error}</p>
174
- {/if}
175
- </div>
176
- </Card.Content>
177
- <Card.Footer class="border-t px-6 py-4">
178
- <Button onclick={updateOrganization} disabled={isUpdatingOrg}>
179
- {isUpdatingOrg ? 'Saving...' : 'Save'}
180
- </Button>
181
- </Card.Footer>
182
- </Card.Root>
@@ -1,25 +0,0 @@
1
- import type { PageServerLoad } from './$types';
2
-
3
- export const load: PageServerLoad = async ({ locals }) => {
4
- const auth = locals.auth;
5
-
6
- if (!auth || auth.type !== 'session') {
7
- throw new Error('No session found');
8
- }
9
-
10
- const databaseAdapter = locals.aphexCMS.databaseAdapter;
11
-
12
- const userProfile = await databaseAdapter.findUserProfileById(auth.user.id);
13
- const userPreferences = userProfile?.preferences || {};
14
-
15
- let hasChildOrganizations = false;
16
- if (auth.organizationId && databaseAdapter.hierarchyEnabled) {
17
- const childOrgs = await databaseAdapter.getChildOrganizations(auth.organizationId);
18
- hasChildOrganizations = childOrgs.length > 0;
19
- }
20
-
21
- return {
22
- userPreferences,
23
- hasChildOrganizations
24
- };
25
- };