create-aphex 0.1.3 → 0.2.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/dist/index.js +9 -88
- package/package.json +4 -4
- package/templates/base/.dockerignore +0 -44
- package/templates/base/.env.example +0 -38
- package/templates/base/CHANGELOG.md +0 -124
- package/templates/base/Dockerfile +0 -69
- package/templates/base/README.md +0 -147
- package/templates/base/aphex.config.ts +0 -45
- package/templates/base/docker-compose.yml +0 -31
- package/templates/base/drizzle/0000_tiny_redwing.sql +0 -226
- package/templates/base/drizzle/meta/0000_snapshot.json +0 -1379
- package/templates/base/drizzle/meta/_journal.json +0 -13
- package/templates/base/drizzle.config.ts +0 -17
- package/templates/base/package.json +0 -81
- package/templates/base/prod.docker-compose.yml +0 -61
- package/templates/base/src/app.css +0 -32
- package/templates/base/src/app.d.ts +0 -26
- package/templates/base/src/app.html +0 -11
- package/templates/base/src/hooks.server.ts +0 -52
- package/templates/base/src/lib/assets/favicon.svg +0 -5
- package/templates/base/src/lib/auth-client.ts +0 -19
- package/templates/base/src/lib/index.ts +0 -1
- package/templates/base/src/lib/schemaTypes/index.ts +0 -11
- package/templates/base/src/lib/schemaTypes/post.ts +0 -47
- package/templates/base/src/lib/server/auth/better-auth/instance.ts +0 -183
- package/templates/base/src/lib/server/auth/index.ts +0 -25
- package/templates/base/src/lib/server/auth/instance.ts +0 -7
- package/templates/base/src/lib/server/auth/service.ts +0 -486
- package/templates/base/src/lib/server/cache/index.ts +0 -10
- package/templates/base/src/lib/server/db/auth-schema.ts +0 -86
- package/templates/base/src/lib/server/db/cms-schema.ts +0 -23
- package/templates/base/src/lib/server/db/index.ts +0 -55
- package/templates/base/src/lib/server/db/schema.ts +0 -3
- package/templates/base/src/lib/server/email/components/EmailLayout.svelte +0 -24
- package/templates/base/src/lib/server/email/components/EmailVerification.svelte +0 -26
- package/templates/base/src/lib/server/email/components/Invitation.svelte +0 -30
- package/templates/base/src/lib/server/email/components/PasswordReset.svelte +0 -28
- package/templates/base/src/lib/server/email/index.ts +0 -28
- package/templates/base/src/lib/server/email/invitation-hook.ts +0 -56
- package/templates/base/src/lib/server/email/renderer.ts +0 -13
- package/templates/base/src/lib/server/email/templates/email-verification.ts +0 -8
- package/templates/base/src/lib/server/email/templates/invitation.ts +0 -8
- package/templates/base/src/lib/server/email/templates/password-reset.ts +0 -7
- package/templates/base/src/lib/server/services/organization.ts +0 -68
- package/templates/base/src/lib/server/storage/index.ts +0 -28
- package/templates/base/src/lib/stores/activeTab.svelte.ts +0 -14
- package/templates/base/src/lib/utils/imageBuilder.ts +0 -3
- package/templates/base/src/lib/utils/slug.ts +0 -33
- package/templates/base/src/routes/(protected)/admin/+layout.server.ts +0 -86
- package/templates/base/src/routes/(protected)/admin/+layout.svelte +0 -78
- package/templates/base/src/routes/(protected)/admin/+page.server.ts +0 -60
- package/templates/base/src/routes/(protected)/admin/+page.svelte +0 -34
- package/templates/base/src/routes/(protected)/admin/organizations/+page.server.ts +0 -39
- package/templates/base/src/routes/(protected)/admin/organizations/+page.svelte +0 -31
- package/templates/base/src/routes/(protected)/admin/organizations/_components/CreateOrganization.svelte +0 -141
- package/templates/base/src/routes/(protected)/admin/organizations/_components/OrganizationsList.svelte +0 -102
- package/templates/base/src/routes/(protected)/admin/settings/+layout.server.ts +0 -42
- package/templates/base/src/routes/(protected)/admin/settings/+layout.svelte +0 -116
- package/templates/base/src/routes/(protected)/admin/settings/+page.server.ts +0 -16
- package/templates/base/src/routes/(protected)/admin/settings/+page.svelte +0 -32
- package/templates/base/src/routes/(protected)/admin/settings/_components/AccountSettings.svelte +0 -177
- package/templates/base/src/routes/(protected)/admin/settings/_components/ApiKeysSettings.svelte +0 -472
- package/templates/base/src/routes/(protected)/admin/settings/_components/OrganizationsSettings.svelte +0 -182
- package/templates/base/src/routes/(protected)/admin/settings/account/+page.server.ts +0 -25
- package/templates/base/src/routes/(protected)/admin/settings/account/+page.svelte +0 -23
- package/templates/base/src/routes/(protected)/admin/settings/api-keys/+page.server.ts +0 -54
- package/templates/base/src/routes/(protected)/admin/settings/api-keys/+page.svelte +0 -14
- package/templates/base/src/routes/(protected)/admin/settings/members/+page.server.ts +0 -31
- package/templates/base/src/routes/(protected)/admin/settings/members/+page.svelte +0 -338
- package/templates/base/src/routes/(protected)/admin/settings/roles/+page.server.ts +0 -17
- package/templates/base/src/routes/(protected)/admin/settings/roles/+page.svelte +0 -339
- package/templates/base/src/routes/+layout.svelte +0 -12
- package/templates/base/src/routes/+page.svelte +0 -2
- package/templates/base/src/routes/api/[...slug]/+server.ts +0 -35
- package/templates/base/src/routes/api/instance-settings/+server.ts +0 -70
- package/templates/base/src/routes/api/invitations/+server.ts +0 -54
- package/templates/base/src/routes/api/invitations/[id]/accept/+server.ts +0 -54
- package/templates/base/src/routes/api/invitations/[id]/reject/+server.ts +0 -49
- package/templates/base/src/routes/api/settings/api-keys/+server.ts +0 -57
- package/templates/base/src/routes/api/settings/api-keys/[id]/+server.ts +0 -53
- package/templates/base/src/routes/god-mode/+layout.server.ts +0 -32
- package/templates/base/src/routes/god-mode/+layout.svelte +0 -155
- package/templates/base/src/routes/god-mode/+page.server.ts +0 -10
- package/templates/base/src/routes/god-mode/+page.svelte +0 -32
- package/templates/base/src/routes/god-mode/_components/CreateOrganization.svelte +0 -141
- package/templates/base/src/routes/god-mode/_components/OrganizationsList.svelte +0 -169
- package/templates/base/src/routes/god-mode/organizations/+page.server.ts +0 -43
- package/templates/base/src/routes/god-mode/organizations/+page.svelte +0 -74
- package/templates/base/src/routes/invitations/+page.server.ts +0 -48
- package/templates/base/src/routes/invitations/+page.svelte +0 -264
- package/templates/base/src/routes/invite/[token]/+page.server.ts +0 -59
- package/templates/base/src/routes/invite/[token]/+page.svelte +0 -109
- package/templates/base/src/routes/login/+page.server.ts +0 -14
- package/templates/base/src/routes/login/+page.svelte +0 -332
- package/templates/base/src/routes/media/[id]/[filename]/+server.ts +0 -3
- package/templates/base/src/routes/reset-password/[token]/+page.server.ts +0 -15
- package/templates/base/src/routes/reset-password/[token]/+page.svelte +0 -179
- package/templates/base/src/routes/verify-email/+page.svelte +0 -41
- package/templates/base/static/images/aphex-darkmode.png +0 -0
- package/templates/base/static/images/aphex-lightmode.png +0 -0
- package/templates/base/static/robots.txt +0 -3
- package/templates/base/svelte.config.js +0 -19
- package/templates/base/tsconfig.json +0 -20
- package/templates/base/vite.config.ts +0 -146
|
@@ -1,339 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import * as Card from '@aphexcms/ui/shadcn/card';
|
|
3
|
-
import * as Sheet from '@aphexcms/ui/shadcn/sheet';
|
|
4
|
-
import { Button } from '@aphexcms/ui/shadcn/button';
|
|
5
|
-
import { Input } from '@aphexcms/ui/shadcn/input';
|
|
6
|
-
import { Textarea } from '@aphexcms/ui/shadcn/textarea';
|
|
7
|
-
import { Label } from '@aphexcms/ui/shadcn/label';
|
|
8
|
-
import { Checkbox } from '@aphexcms/ui/shadcn/checkbox';
|
|
9
|
-
import { Badge } from '@aphexcms/ui/shadcn/badge';
|
|
10
|
-
import { Separator } from '@aphexcms/ui/shadcn/separator';
|
|
11
|
-
import { confirmDialog, usePermissions } from '@aphexcms/cms-core/client';
|
|
12
|
-
import { ALL_CAPABILITIES } from '@aphexcms/cms-core';
|
|
13
|
-
import type { Capability, Role } from '@aphexcms/cms-core';
|
|
14
|
-
import { roles as rolesApi } from '@aphexcms/cms-core/client';
|
|
15
|
-
import { invalidateAll } from '$app/navigation';
|
|
16
|
-
import { toast } from 'svelte-sonner';
|
|
17
|
-
import { Shield, Plus, Pencil, Trash2, Lock } from '@lucide/svelte';
|
|
18
|
-
import type { PageData } from './$types';
|
|
19
|
-
|
|
20
|
-
let { data }: { data: PageData } = $props();
|
|
21
|
-
|
|
22
|
-
const roles = $derived(data.roles as Role[]);
|
|
23
|
-
// Read from the shared permissions context instead of a separate server
|
|
24
|
-
// flag — single source of truth, stays in sync if the role changes.
|
|
25
|
-
const perms = usePermissions();
|
|
26
|
-
const canManageRoles = $derived(perms.can('role.manage'));
|
|
27
|
-
|
|
28
|
-
type EditorState = { kind: 'closed' } | { kind: 'create' } | { kind: 'edit'; role: Role };
|
|
29
|
-
|
|
30
|
-
let editor = $state<EditorState>({ kind: 'closed' });
|
|
31
|
-
let form = $state({
|
|
32
|
-
name: '',
|
|
33
|
-
description: '',
|
|
34
|
-
capabilities: new Set<Capability>()
|
|
35
|
-
});
|
|
36
|
-
let saving = $state(false);
|
|
37
|
-
|
|
38
|
-
// Capability groupings for the picker. Any capability not listed here falls
|
|
39
|
-
// into "Other" so new additions don't disappear silently.
|
|
40
|
-
const CAPABILITY_GROUPS: { title: string; prefix: string }[] = [
|
|
41
|
-
{ title: 'Documents', prefix: 'document.' },
|
|
42
|
-
{ title: 'Assets', prefix: 'asset.' },
|
|
43
|
-
{ title: 'Members', prefix: 'member.' },
|
|
44
|
-
{ title: 'Organization', prefix: 'org.' },
|
|
45
|
-
{ title: 'Keys & Roles', prefix: '' } // catch-all, resolved below
|
|
46
|
-
];
|
|
47
|
-
|
|
48
|
-
function groupedCapabilities() {
|
|
49
|
-
const groups: Record<string, Capability[]> = {};
|
|
50
|
-
for (const cap of ALL_CAPABILITIES) {
|
|
51
|
-
const match = CAPABILITY_GROUPS.find((g) => g.prefix && cap.startsWith(g.prefix));
|
|
52
|
-
const title = match?.title ?? 'Keys & Roles';
|
|
53
|
-
(groups[title] ??= []).push(cap);
|
|
54
|
-
}
|
|
55
|
-
return CAPABILITY_GROUPS.map((g) => ({
|
|
56
|
-
title: g.title,
|
|
57
|
-
items: groups[g.title] ?? []
|
|
58
|
-
})).filter((g) => g.items.length > 0);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function capabilityLabel(cap: Capability): string {
|
|
62
|
-
// "document.publish" → "Publish"
|
|
63
|
-
const suffix = cap.split('.').slice(1).join('.');
|
|
64
|
-
return suffix.charAt(0).toUpperCase() + suffix.slice(1);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
function openCreate() {
|
|
68
|
-
form = { name: '', description: '', capabilities: new Set() };
|
|
69
|
-
editor = { kind: 'create' };
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function openEdit(role: Role) {
|
|
73
|
-
form = {
|
|
74
|
-
name: role.name,
|
|
75
|
-
description: role.description ?? '',
|
|
76
|
-
capabilities: new Set(role.capabilities)
|
|
77
|
-
};
|
|
78
|
-
editor = { kind: 'edit', role };
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function closeEditor() {
|
|
82
|
-
editor = { kind: 'closed' };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Write caps imply their matching read on the same resource family. Mirror
|
|
86
|
-
// the server-side normalization visually so users see the read checkbox
|
|
87
|
-
// light up instead of being surprised by it reappearing post-save.
|
|
88
|
-
const READ_IMPLIED_BY: Partial<Record<Capability, Capability>> = {
|
|
89
|
-
'document.create': 'document.read',
|
|
90
|
-
'document.update': 'document.read',
|
|
91
|
-
'document.delete': 'document.read',
|
|
92
|
-
'document.publish': 'document.read',
|
|
93
|
-
'document.unpublish': 'document.read',
|
|
94
|
-
'asset.upload': 'asset.read',
|
|
95
|
-
'asset.delete': 'asset.read'
|
|
96
|
-
};
|
|
97
|
-
|
|
98
|
-
const WRITES_NEEDING: Record<Capability, Capability[]> = Object.entries(READ_IMPLIED_BY).reduce(
|
|
99
|
-
(acc, [write, read]) => {
|
|
100
|
-
if (!read) return acc;
|
|
101
|
-
(acc[read as Capability] ??= []).push(write as Capability);
|
|
102
|
-
return acc;
|
|
103
|
-
},
|
|
104
|
-
{} as Record<Capability, Capability[]>
|
|
105
|
-
);
|
|
106
|
-
|
|
107
|
-
function toggleCapability(cap: Capability, next: boolean) {
|
|
108
|
-
const updated = new Set(form.capabilities);
|
|
109
|
-
if (next) {
|
|
110
|
-
updated.add(cap);
|
|
111
|
-
const implied = READ_IMPLIED_BY[cap];
|
|
112
|
-
if (implied) updated.add(implied);
|
|
113
|
-
} else {
|
|
114
|
-
// Don't allow unchecking a read cap while a dependent write is selected —
|
|
115
|
-
// the server would re-add it anyway.
|
|
116
|
-
const dependents = WRITES_NEEDING[cap] ?? [];
|
|
117
|
-
if (dependents.some((w) => updated.has(w))) return;
|
|
118
|
-
updated.delete(cap);
|
|
119
|
-
}
|
|
120
|
-
form.capabilities = updated;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
function isImpliedRead(cap: Capability): boolean {
|
|
124
|
-
const dependents = WRITES_NEEDING[cap] ?? [];
|
|
125
|
-
return dependents.some((w) => form.capabilities.has(w));
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async function save() {
|
|
129
|
-
saving = true;
|
|
130
|
-
try {
|
|
131
|
-
const capabilities = Array.from(form.capabilities);
|
|
132
|
-
if (editor.kind === 'create') {
|
|
133
|
-
if (!form.name.trim()) {
|
|
134
|
-
toast.error('Name is required');
|
|
135
|
-
return;
|
|
136
|
-
}
|
|
137
|
-
const result = await rolesApi.create({
|
|
138
|
-
name: form.name.trim(),
|
|
139
|
-
description: form.description.trim() || null,
|
|
140
|
-
capabilities
|
|
141
|
-
});
|
|
142
|
-
if (!result.success) throw new Error(result.error || 'Failed to create role');
|
|
143
|
-
toast.success(`Role "${form.name.trim()}" created`);
|
|
144
|
-
} else if (editor.kind === 'edit') {
|
|
145
|
-
const result = await rolesApi.update(editor.role.name, {
|
|
146
|
-
description: form.description.trim() || null,
|
|
147
|
-
capabilities
|
|
148
|
-
});
|
|
149
|
-
if (!result.success) throw new Error(result.error || 'Failed to update role');
|
|
150
|
-
toast.success(`Role "${editor.role.name}" updated`);
|
|
151
|
-
}
|
|
152
|
-
closeEditor();
|
|
153
|
-
await invalidateAll();
|
|
154
|
-
} catch (err) {
|
|
155
|
-
toast.error(err instanceof Error ? err.message : 'Failed to save role');
|
|
156
|
-
} finally {
|
|
157
|
-
saving = false;
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async function remove(role: Role) {
|
|
162
|
-
const confirmed = await confirmDialog({
|
|
163
|
-
title: `Delete role "${role.name}"?`,
|
|
164
|
-
description:
|
|
165
|
-
'This cannot be undone. Members and pending invitations using this role will block the deletion.',
|
|
166
|
-
confirmText: 'Delete',
|
|
167
|
-
variant: 'destructive'
|
|
168
|
-
});
|
|
169
|
-
if (!confirmed) return;
|
|
170
|
-
|
|
171
|
-
try {
|
|
172
|
-
const result = await rolesApi.remove(role.name);
|
|
173
|
-
if (!result.success) throw new Error(result.error || 'Failed to delete role');
|
|
174
|
-
toast.success(`Role "${role.name}" deleted`);
|
|
175
|
-
await invalidateAll();
|
|
176
|
-
} catch (err) {
|
|
177
|
-
toast.error(err instanceof Error ? err.message : 'Failed to delete role');
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
</script>
|
|
181
|
-
|
|
182
|
-
<svelte:head>
|
|
183
|
-
<title>Aphex CMS - Roles</title>
|
|
184
|
-
</svelte:head>
|
|
185
|
-
|
|
186
|
-
<div class="grid gap-6">
|
|
187
|
-
<div class="flex items-start justify-between gap-4">
|
|
188
|
-
<div class="hidden sm:block">
|
|
189
|
-
<h2 class="text-xl font-semibold">Roles</h2>
|
|
190
|
-
<p class="text-muted-foreground text-sm">
|
|
191
|
-
Define which members can do what. Built-in roles are always available; add custom roles to
|
|
192
|
-
match how your team works.
|
|
193
|
-
</p>
|
|
194
|
-
</div>
|
|
195
|
-
{#if canManageRoles}
|
|
196
|
-
<Button onclick={openCreate} class="shrink-0">
|
|
197
|
-
<Plus class="mr-1 h-4 w-4" /> New role
|
|
198
|
-
</Button>
|
|
199
|
-
{/if}
|
|
200
|
-
</div>
|
|
201
|
-
|
|
202
|
-
<div class="grid gap-3">
|
|
203
|
-
{#each roles as role (role.id)}
|
|
204
|
-
<Card.Root>
|
|
205
|
-
<Card.Content class="flex items-center gap-4 py-4">
|
|
206
|
-
<div class="bg-muted flex h-10 w-10 items-center justify-center rounded-md">
|
|
207
|
-
{#if role.isBuiltIn}
|
|
208
|
-
<Lock class="text-muted-foreground h-4 w-4" />
|
|
209
|
-
{:else}
|
|
210
|
-
<Shield class="text-muted-foreground h-4 w-4" />
|
|
211
|
-
{/if}
|
|
212
|
-
</div>
|
|
213
|
-
<div class="min-w-0 flex-1">
|
|
214
|
-
<div class="flex items-center gap-2">
|
|
215
|
-
<p class="truncate text-sm font-semibold capitalize">{role.name}</p>
|
|
216
|
-
{#if role.isBuiltIn}
|
|
217
|
-
<Badge variant="secondary" class="text-[10px]">Built-in</Badge>
|
|
218
|
-
{/if}
|
|
219
|
-
<Badge variant="outline" class="text-[10px]">
|
|
220
|
-
{role.capabilities.length} capabilit{role.capabilities.length === 1 ? 'y' : 'ies'}
|
|
221
|
-
</Badge>
|
|
222
|
-
</div>
|
|
223
|
-
{#if role.description}
|
|
224
|
-
<p class="text-muted-foreground mt-0.5 truncate text-xs">{role.description}</p>
|
|
225
|
-
{/if}
|
|
226
|
-
</div>
|
|
227
|
-
{#if canManageRoles}
|
|
228
|
-
<div class="flex items-center gap-2">
|
|
229
|
-
<Button variant="outline" size="sm" onclick={() => openEdit(role)}>
|
|
230
|
-
<Pencil class="mr-1 h-3.5 w-3.5" /> Edit
|
|
231
|
-
</Button>
|
|
232
|
-
{#if !role.isBuiltIn}
|
|
233
|
-
<Button variant="outline" size="sm" onclick={() => remove(role)}>
|
|
234
|
-
<Trash2 class="text-destructive mr-1 h-3.5 w-3.5" /> Delete
|
|
235
|
-
</Button>
|
|
236
|
-
{/if}
|
|
237
|
-
</div>
|
|
238
|
-
{/if}
|
|
239
|
-
</Card.Content>
|
|
240
|
-
</Card.Root>
|
|
241
|
-
{/each}
|
|
242
|
-
</div>
|
|
243
|
-
</div>
|
|
244
|
-
|
|
245
|
-
<Sheet.Root
|
|
246
|
-
open={editor.kind !== 'closed'}
|
|
247
|
-
onOpenChange={(o) => {
|
|
248
|
-
if (!o) closeEditor();
|
|
249
|
-
}}
|
|
250
|
-
>
|
|
251
|
-
<Sheet.Content class="flex w-full flex-col gap-0 p-0 sm:max-w-lg">
|
|
252
|
-
<Sheet.Header class="border-b p-6">
|
|
253
|
-
<Sheet.Title>
|
|
254
|
-
{editor.kind === 'create'
|
|
255
|
-
? 'Create role'
|
|
256
|
-
: `Edit "${editor.kind === 'edit' ? editor.role.name : ''}"`}
|
|
257
|
-
</Sheet.Title>
|
|
258
|
-
<Sheet.Description>
|
|
259
|
-
{#if editor.kind === 'edit' && editor.role.isBuiltIn}
|
|
260
|
-
Built-in role. Capabilities can be tuned; the name is fixed.
|
|
261
|
-
{:else}
|
|
262
|
-
Pick the capabilities this role grants. Members assigned to this role will gain every
|
|
263
|
-
checked permission.
|
|
264
|
-
{/if}
|
|
265
|
-
</Sheet.Description>
|
|
266
|
-
</Sheet.Header>
|
|
267
|
-
|
|
268
|
-
<div class="flex-1 space-y-6 overflow-y-auto p-6">
|
|
269
|
-
<div class="space-y-2">
|
|
270
|
-
<Label for="role-name">Name</Label>
|
|
271
|
-
<Input
|
|
272
|
-
id="role-name"
|
|
273
|
-
bind:value={form.name}
|
|
274
|
-
disabled={editor.kind === 'edit'}
|
|
275
|
-
placeholder="e.g. translator"
|
|
276
|
-
/>
|
|
277
|
-
{#if editor.kind === 'create'}
|
|
278
|
-
<p class="text-muted-foreground text-xs">
|
|
279
|
-
Letters, numbers, spaces, hyphens, or underscores. Used when assigning members.
|
|
280
|
-
</p>
|
|
281
|
-
{/if}
|
|
282
|
-
</div>
|
|
283
|
-
|
|
284
|
-
<div class="space-y-2">
|
|
285
|
-
<Label for="role-description">Description</Label>
|
|
286
|
-
<Textarea
|
|
287
|
-
id="role-description"
|
|
288
|
-
bind:value={form.description}
|
|
289
|
-
placeholder="What does this role do?"
|
|
290
|
-
rows={2}
|
|
291
|
-
/>
|
|
292
|
-
</div>
|
|
293
|
-
|
|
294
|
-
<Separator />
|
|
295
|
-
|
|
296
|
-
<div class="space-y-4">
|
|
297
|
-
<div class="flex items-center justify-between">
|
|
298
|
-
<Label>Capabilities</Label>
|
|
299
|
-
<span class="text-muted-foreground text-xs">
|
|
300
|
-
{form.capabilities.size} of {ALL_CAPABILITIES.length} selected
|
|
301
|
-
</span>
|
|
302
|
-
</div>
|
|
303
|
-
|
|
304
|
-
{#each groupedCapabilities() as group (group.title)}
|
|
305
|
-
<div class="space-y-2">
|
|
306
|
-
<p class="text-muted-foreground text-xs font-semibold tracking-wider uppercase">
|
|
307
|
-
{group.title}
|
|
308
|
-
</p>
|
|
309
|
-
<div class="grid gap-2">
|
|
310
|
-
{#each group.items as cap (cap)}
|
|
311
|
-
{@const implied = isImpliedRead(cap)}
|
|
312
|
-
<label
|
|
313
|
-
class="hover:bg-muted flex cursor-pointer items-center gap-3 rounded-md border px-3 py-2"
|
|
314
|
-
>
|
|
315
|
-
<Checkbox
|
|
316
|
-
checked={form.capabilities.has(cap)}
|
|
317
|
-
disabled={implied}
|
|
318
|
-
onCheckedChange={(v) => toggleCapability(cap, v === true)}
|
|
319
|
-
/>
|
|
320
|
-
<div class="flex-1">
|
|
321
|
-
<p class="text-sm font-medium">{capabilityLabel(cap)}</p>
|
|
322
|
-
<p class="text-muted-foreground font-mono text-[10px]">{cap}</p>
|
|
323
|
-
</div>
|
|
324
|
-
</label>
|
|
325
|
-
{/each}
|
|
326
|
-
</div>
|
|
327
|
-
</div>
|
|
328
|
-
{/each}
|
|
329
|
-
</div>
|
|
330
|
-
</div>
|
|
331
|
-
|
|
332
|
-
<div class="flex items-center justify-end gap-2 border-t p-6">
|
|
333
|
-
<Button variant="outline" onclick={closeEditor} disabled={saving}>Cancel</Button>
|
|
334
|
-
<Button onclick={save} disabled={saving}>
|
|
335
|
-
{saving ? 'Saving...' : editor.kind === 'create' ? 'Create role' : 'Save changes'}
|
|
336
|
-
</Button>
|
|
337
|
-
</div>
|
|
338
|
-
</Sheet.Content>
|
|
339
|
-
</Sheet.Root>
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import type { RequestHandler } from '@sveltejs/kit';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Catch-all forwarder: any `/api/**` request that doesn't match a specific
|
|
5
|
-
* `+server.ts` falls through here and gets handed to the Aphex Hono app.
|
|
6
|
-
*
|
|
7
|
-
* SvelteKit prefers specific routes over catch-alls, so existing per-endpoint
|
|
8
|
-
* shims continue to win during the migration. Once a route is ported into
|
|
9
|
-
* the Hono app, deleting its specific `+server.ts` flips traffic over.
|
|
10
|
-
*/
|
|
11
|
-
const handler: RequestHandler = ({ request, locals, getClientAddress }) => {
|
|
12
|
-
const apiApp = locals.aphexCMS?.apiApp;
|
|
13
|
-
if (!apiApp) {
|
|
14
|
-
return new Response('CMS not initialized', { status: 503 });
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
try {
|
|
18
|
-
request.headers.set('x-forwarded-for', getClientAddress());
|
|
19
|
-
} catch {
|
|
20
|
-
// Some runtimes expose immutable Request headers; non-fatal.
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
return apiApp.fetch(request, {
|
|
24
|
-
aphexCMS: locals.aphexCMS,
|
|
25
|
-
auth: locals.auth ?? null
|
|
26
|
-
});
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export const GET = handler;
|
|
30
|
-
export const POST = handler;
|
|
31
|
-
export const PUT = handler;
|
|
32
|
-
export const DELETE = handler;
|
|
33
|
-
export const PATCH = handler;
|
|
34
|
-
export const OPTIONS = handler;
|
|
35
|
-
export const HEAD = handler;
|
|
@@ -1,70 +0,0 @@
|
|
|
1
|
-
import { json } from '@sveltejs/kit';
|
|
2
|
-
import type { RequestHandler } from '@sveltejs/kit';
|
|
3
|
-
|
|
4
|
-
// GET /api/instance-settings — Get instance settings
|
|
5
|
-
export const GET: RequestHandler = async ({ locals }) => {
|
|
6
|
-
try {
|
|
7
|
-
const { databaseAdapter } = locals.aphexCMS;
|
|
8
|
-
const auth = locals.auth;
|
|
9
|
-
|
|
10
|
-
if (!auth || auth.type !== 'session') {
|
|
11
|
-
return json(
|
|
12
|
-
{ success: false, error: 'Unauthorized', message: 'Session authentication required' },
|
|
13
|
-
{ status: 401 }
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const settings = await databaseAdapter.getInstanceSettings();
|
|
18
|
-
return json({ success: true, data: settings });
|
|
19
|
-
} catch (error) {
|
|
20
|
-
console.error('Failed to fetch instance settings:', error);
|
|
21
|
-
return json(
|
|
22
|
-
{
|
|
23
|
-
success: false,
|
|
24
|
-
error: 'Failed to fetch instance settings',
|
|
25
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
26
|
-
},
|
|
27
|
-
{ status: 500 }
|
|
28
|
-
);
|
|
29
|
-
}
|
|
30
|
-
};
|
|
31
|
-
|
|
32
|
-
// PATCH /api/instance-settings — Update instance settings (super_admin only)
|
|
33
|
-
export const PATCH: RequestHandler = async ({ request, locals }) => {
|
|
34
|
-
try {
|
|
35
|
-
const { databaseAdapter } = locals.aphexCMS;
|
|
36
|
-
const auth = locals.auth;
|
|
37
|
-
|
|
38
|
-
if (!auth || auth.type !== 'session') {
|
|
39
|
-
return json(
|
|
40
|
-
{ success: false, error: 'Unauthorized', message: 'Session authentication required' },
|
|
41
|
-
{ status: 401 }
|
|
42
|
-
);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (auth.user.role !== 'super_admin') {
|
|
46
|
-
return json(
|
|
47
|
-
{
|
|
48
|
-
success: false,
|
|
49
|
-
error: 'Forbidden',
|
|
50
|
-
message: 'Only super admins can update instance settings'
|
|
51
|
-
},
|
|
52
|
-
{ status: 403 }
|
|
53
|
-
);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const body = await request.json();
|
|
57
|
-
const updated = await databaseAdapter.updateInstanceSettings(body);
|
|
58
|
-
return json({ success: true, data: updated });
|
|
59
|
-
} catch (error) {
|
|
60
|
-
console.error('Failed to update instance settings:', error);
|
|
61
|
-
return json(
|
|
62
|
-
{
|
|
63
|
-
success: false,
|
|
64
|
-
error: 'Failed to update instance settings',
|
|
65
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
66
|
-
},
|
|
67
|
-
{ status: 500 }
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { json } from '@sveltejs/kit';
|
|
2
|
-
import type { RequestHandler } from '@sveltejs/kit';
|
|
3
|
-
|
|
4
|
-
// GET /api/invitations — List all pending invitations for the authenticated user
|
|
5
|
-
export const GET: RequestHandler = async ({ locals }) => {
|
|
6
|
-
try {
|
|
7
|
-
const { databaseAdapter } = locals.aphexCMS;
|
|
8
|
-
const auth = locals.auth;
|
|
9
|
-
|
|
10
|
-
if (!auth || auth.type === 'api_key') {
|
|
11
|
-
return json(
|
|
12
|
-
{ success: false, error: 'Unauthorized', message: 'Session authentication required' },
|
|
13
|
-
{ status: 401 }
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const allInvitations = await databaseAdapter.findInvitationsByEmail(auth.user.email);
|
|
18
|
-
|
|
19
|
-
// Filter to only pending (not accepted, not expired)
|
|
20
|
-
const now = new Date();
|
|
21
|
-
const pending = allInvitations.filter(
|
|
22
|
-
(inv) => inv.acceptedAt === null && new Date(inv.expiresAt) > now
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
// Enrich with organization names
|
|
26
|
-
const enriched = await Promise.all(
|
|
27
|
-
pending.map(async (inv) => {
|
|
28
|
-
const org = await databaseAdapter.findOrganizationById(inv.organizationId);
|
|
29
|
-
return {
|
|
30
|
-
id: inv.id,
|
|
31
|
-
organizationId: inv.organizationId,
|
|
32
|
-
organizationName: org?.name ?? 'Unknown',
|
|
33
|
-
organizationSlug: org?.slug ?? '',
|
|
34
|
-
role: inv.role,
|
|
35
|
-
email: inv.email,
|
|
36
|
-
expiresAt: inv.expiresAt,
|
|
37
|
-
createdAt: inv.createdAt
|
|
38
|
-
};
|
|
39
|
-
})
|
|
40
|
-
);
|
|
41
|
-
|
|
42
|
-
return json({ success: true, data: enriched });
|
|
43
|
-
} catch (error) {
|
|
44
|
-
console.error('Failed to fetch invitations:', error);
|
|
45
|
-
return json(
|
|
46
|
-
{
|
|
47
|
-
success: false,
|
|
48
|
-
error: 'Failed to fetch invitations',
|
|
49
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
50
|
-
},
|
|
51
|
-
{ status: 500 }
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { json } from '@sveltejs/kit';
|
|
2
|
-
import type { RequestHandler } from '@sveltejs/kit';
|
|
3
|
-
|
|
4
|
-
// POST /api/invitations/[id]/accept — Accept a pending invitation
|
|
5
|
-
export const POST: RequestHandler = async ({ params, locals }) => {
|
|
6
|
-
try {
|
|
7
|
-
const { databaseAdapter } = locals.aphexCMS;
|
|
8
|
-
const auth = locals.auth;
|
|
9
|
-
|
|
10
|
-
if (!auth || auth.type === 'api_key') {
|
|
11
|
-
return json(
|
|
12
|
-
{ success: false, error: 'Unauthorized', message: 'Session authentication required' },
|
|
13
|
-
{ status: 401 }
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const invitationId = params.id;
|
|
18
|
-
|
|
19
|
-
// Find the invitation by looking up user's invitations and matching ID
|
|
20
|
-
const allInvitations = await databaseAdapter.findInvitationsByEmail(auth.user.email);
|
|
21
|
-
const invitation = allInvitations.find((inv) => inv.id === invitationId);
|
|
22
|
-
|
|
23
|
-
if (!invitation) {
|
|
24
|
-
return json({ success: false, error: 'Invitation not found' }, { status: 404 });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (invitation.acceptedAt !== null) {
|
|
28
|
-
return json({ success: false, error: 'Invitation already accepted' }, { status: 400 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (new Date(invitation.expiresAt) < new Date()) {
|
|
32
|
-
return json({ success: false, error: 'Invitation has expired' }, { status: 400 });
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
// Accept the invitation (creates membership)
|
|
36
|
-
await databaseAdapter.acceptInvitation(invitation.token, auth.user.id);
|
|
37
|
-
|
|
38
|
-
return json({
|
|
39
|
-
success: true,
|
|
40
|
-
data: { organizationId: invitation.organizationId },
|
|
41
|
-
message: 'Invitation accepted'
|
|
42
|
-
});
|
|
43
|
-
} catch (error) {
|
|
44
|
-
console.error('Failed to accept invitation:', error);
|
|
45
|
-
return json(
|
|
46
|
-
{
|
|
47
|
-
success: false,
|
|
48
|
-
error: 'Failed to accept invitation',
|
|
49
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
50
|
-
},
|
|
51
|
-
{ status: 500 }
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
};
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import { json } from '@sveltejs/kit';
|
|
2
|
-
import type { RequestHandler } from '@sveltejs/kit';
|
|
3
|
-
|
|
4
|
-
// POST /api/invitations/[id]/reject — Reject/decline a pending invitation
|
|
5
|
-
export const POST: RequestHandler = async ({ params, locals }) => {
|
|
6
|
-
try {
|
|
7
|
-
const { databaseAdapter } = locals.aphexCMS;
|
|
8
|
-
const auth = locals.auth;
|
|
9
|
-
|
|
10
|
-
if (!auth || auth.type === 'api_key') {
|
|
11
|
-
return json(
|
|
12
|
-
{ success: false, error: 'Unauthorized', message: 'Session authentication required' },
|
|
13
|
-
{ status: 401 }
|
|
14
|
-
);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
const invitationId = params.id;
|
|
18
|
-
|
|
19
|
-
// Verify this invitation belongs to the current user
|
|
20
|
-
const allInvitations = await databaseAdapter.findInvitationsByEmail(auth.user.email);
|
|
21
|
-
const invitation = allInvitations.find((inv) => inv.id === invitationId);
|
|
22
|
-
|
|
23
|
-
if (!invitation) {
|
|
24
|
-
return json({ success: false, error: 'Invitation not found' }, { status: 404 });
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
if (invitation.acceptedAt !== null) {
|
|
28
|
-
return json({ success: false, error: 'Invitation already accepted' }, { status: 400 });
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
// Delete the invitation
|
|
32
|
-
await databaseAdapter.deleteInvitation(invitationId!);
|
|
33
|
-
|
|
34
|
-
return json({
|
|
35
|
-
success: true,
|
|
36
|
-
message: 'Invitation declined'
|
|
37
|
-
});
|
|
38
|
-
} catch (error) {
|
|
39
|
-
console.error('Failed to reject invitation:', error);
|
|
40
|
-
return json(
|
|
41
|
-
{
|
|
42
|
-
success: false,
|
|
43
|
-
error: 'Failed to reject invitation',
|
|
44
|
-
message: error instanceof Error ? error.message : 'Unknown error'
|
|
45
|
-
},
|
|
46
|
-
{ status: 500 }
|
|
47
|
-
);
|
|
48
|
-
}
|
|
49
|
-
};
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
import { json } from '@sveltejs/kit';
|
|
2
|
-
import type { RequestHandler } from './$types';
|
|
3
|
-
import { authService } from '$lib/server/auth/service';
|
|
4
|
-
import { createApiKeyRequest } from '@aphexcms/cms-core/api/schemas/api-keys';
|
|
5
|
-
import { hasCapability } from '@aphexcms/cms-core';
|
|
6
|
-
|
|
7
|
-
// GET - List user's API keys
|
|
8
|
-
export const GET: RequestHandler = async ({ locals }) => {
|
|
9
|
-
if (!locals.auth || locals.auth.type !== 'session') {
|
|
10
|
-
return json({ error: 'Unauthorized' }, { status: 401 });
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
try {
|
|
14
|
-
const { databaseAdapter } = locals.aphexCMS;
|
|
15
|
-
const apiKeys = await authService.listApiKeys(databaseAdapter, locals.auth.user.id);
|
|
16
|
-
return json({ success: true, data: apiKeys });
|
|
17
|
-
} catch (error) {
|
|
18
|
-
console.error('Error fetching API keys:', error);
|
|
19
|
-
return json({ error: 'Failed to fetch API keys' }, { status: 500 });
|
|
20
|
-
}
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
// POST - Create new API key
|
|
24
|
-
export const POST: RequestHandler = async ({ request, locals }) => {
|
|
25
|
-
if (!locals.auth || locals.auth.type !== 'session') {
|
|
26
|
-
return json({ error: 'Unauthorized' }, { status: 401 });
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const auth = locals.auth;
|
|
30
|
-
|
|
31
|
-
try {
|
|
32
|
-
// Capability-driven — custom roles with apiKey.manage are welcome.
|
|
33
|
-
if (!hasCapability(auth, 'apiKey.manage')) {
|
|
34
|
-
return json(
|
|
35
|
-
{
|
|
36
|
-
error: 'Forbidden',
|
|
37
|
-
message: 'You do not have permission to create API keys'
|
|
38
|
-
},
|
|
39
|
-
{ status: 403 }
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
const rawBody = await request.json();
|
|
44
|
-
const parsed = createApiKeyRequest.safeParse(rawBody);
|
|
45
|
-
if (!parsed.success) {
|
|
46
|
-
return json({ error: 'Invalid input', issues: parsed.error.issues }, { status: 400 });
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// Create API key bound to the user's current active organization
|
|
50
|
-
const apiKey = await authService.createApiKey(auth.user.id, auth.organizationId, parsed.data);
|
|
51
|
-
|
|
52
|
-
return json({ success: true, data: { apiKey } });
|
|
53
|
-
} catch (error) {
|
|
54
|
-
console.error('Error creating API key:', error);
|
|
55
|
-
return json({ error: 'Failed to create API key' }, { status: 500 });
|
|
56
|
-
}
|
|
57
|
-
};
|