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.
- 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,31 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { page } from '$app/state';
|
|
3
|
-
import CreateOrganization from './_components/CreateOrganization.svelte';
|
|
4
|
-
import OrganizationsList from './_components/OrganizationsList.svelte';
|
|
5
|
-
import type { PageData } from './$types';
|
|
6
|
-
|
|
7
|
-
let { data }: { data: PageData } = $props();
|
|
8
|
-
|
|
9
|
-
const userRole = $derived(page.data?.sidebarData?.user?.role);
|
|
10
|
-
const isSuperAdmin = $derived(userRole === 'super_admin');
|
|
11
|
-
</script>
|
|
12
|
-
|
|
13
|
-
<svelte:head>
|
|
14
|
-
<title>Aphex CMS - Organizations</title>
|
|
15
|
-
</svelte:head>
|
|
16
|
-
|
|
17
|
-
<div class="flex flex-1 flex-col gap-4 p-4 md:gap-8 md:p-8">
|
|
18
|
-
<div class="mx-auto flex w-full max-w-6xl items-center justify-between">
|
|
19
|
-
<div>
|
|
20
|
-
<h1 class="text-3xl font-semibold">Organizations</h1>
|
|
21
|
-
<p class="text-muted-foreground">Manage your organizations</p>
|
|
22
|
-
</div>
|
|
23
|
-
{#if isSuperAdmin}
|
|
24
|
-
<CreateOrganization />
|
|
25
|
-
{/if}
|
|
26
|
-
</div>
|
|
27
|
-
|
|
28
|
-
<div class="mx-auto grid w-full max-w-6xl gap-6">
|
|
29
|
-
<OrganizationsList orgs={data.organizations} />
|
|
30
|
-
</div>
|
|
31
|
-
</div>
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { goto, invalidateAll } from '$app/navigation';
|
|
3
|
-
import { Button } from '@aphexcms/ui/shadcn/button';
|
|
4
|
-
import { Input } from '@aphexcms/ui/shadcn/input';
|
|
5
|
-
import { Label } from '@aphexcms/ui/shadcn/label';
|
|
6
|
-
import * as Dialog from '@aphexcms/ui/shadcn/dialog';
|
|
7
|
-
import { organizations } from '@aphexcms/cms-core/client';
|
|
8
|
-
|
|
9
|
-
let open = $state(false);
|
|
10
|
-
let name = $state('');
|
|
11
|
-
let slug = $state('');
|
|
12
|
-
let isSubmitting = $state(false);
|
|
13
|
-
let error = $state<string | null>(null);
|
|
14
|
-
|
|
15
|
-
function generateSlug(text: string): string {
|
|
16
|
-
return text
|
|
17
|
-
.toLowerCase()
|
|
18
|
-
.trim()
|
|
19
|
-
.replace(/[^\w\s-]/g, '')
|
|
20
|
-
.replace(/[\s_-]+/g, '-')
|
|
21
|
-
.replace(/^-+|-+$/g, '');
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function handleNameInput(event: Event) {
|
|
25
|
-
const target = event.target as HTMLInputElement;
|
|
26
|
-
name = target.value;
|
|
27
|
-
if (!slug || slug === generateSlug(name.slice(0, -1))) {
|
|
28
|
-
slug = generateSlug(name);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
function handleSlugInput(event: Event) {
|
|
33
|
-
const target = event.target as HTMLInputElement;
|
|
34
|
-
slug = target.value.toLowerCase().replace(/[^a-z0-9-]/g, '');
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function resetForm() {
|
|
38
|
-
name = '';
|
|
39
|
-
slug = '';
|
|
40
|
-
error = null;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function handleSubmit(event: Event) {
|
|
44
|
-
event.preventDefault();
|
|
45
|
-
error = null;
|
|
46
|
-
|
|
47
|
-
if (!name.trim() || !slug.trim()) {
|
|
48
|
-
error = 'Name and slug are required';
|
|
49
|
-
return;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
isSubmitting = true;
|
|
53
|
-
try {
|
|
54
|
-
const result = await organizations.create({
|
|
55
|
-
name: name.trim(),
|
|
56
|
-
slug: slug.trim()
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
if (!result.success || !result.data) {
|
|
60
|
-
throw new Error('Failed to create organization');
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
resetForm();
|
|
64
|
-
open = false;
|
|
65
|
-
await invalidateAll();
|
|
66
|
-
await goto(`/admin?orgId=${result.data.id}`);
|
|
67
|
-
} catch (err) {
|
|
68
|
-
error = err instanceof Error ? err.message : 'Failed to create organization';
|
|
69
|
-
} finally {
|
|
70
|
-
isSubmitting = false;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
</script>
|
|
74
|
-
|
|
75
|
-
<Dialog.Root bind:open onOpenChange={(v) => !v && resetForm()}>
|
|
76
|
-
<Dialog.Trigger>
|
|
77
|
-
{#snippet child({ props })}
|
|
78
|
-
<Button {...props}>Create Organization</Button>
|
|
79
|
-
{/snippet}
|
|
80
|
-
</Dialog.Trigger>
|
|
81
|
-
<Dialog.Content class="sm:max-w-[480px]">
|
|
82
|
-
<Dialog.Header>
|
|
83
|
-
<Dialog.Title>Create Organization</Dialog.Title>
|
|
84
|
-
<Dialog.Description>Create a new organization for your instance</Dialog.Description>
|
|
85
|
-
</Dialog.Header>
|
|
86
|
-
<form onsubmit={handleSubmit} class="grid gap-4 py-4">
|
|
87
|
-
<div>
|
|
88
|
-
<Label for="org-name">Organization name</Label>
|
|
89
|
-
<Input
|
|
90
|
-
id="org-name"
|
|
91
|
-
value={name}
|
|
92
|
-
oninput={handleNameInput}
|
|
93
|
-
placeholder="My Organization"
|
|
94
|
-
disabled={isSubmitting}
|
|
95
|
-
class="mt-1"
|
|
96
|
-
/>
|
|
97
|
-
</div>
|
|
98
|
-
<div>
|
|
99
|
-
<Label for="org-slug">Slug</Label>
|
|
100
|
-
<div class="mt-1 flex gap-2">
|
|
101
|
-
<Input
|
|
102
|
-
id="org-slug"
|
|
103
|
-
value={slug}
|
|
104
|
-
oninput={handleSlugInput}
|
|
105
|
-
placeholder="my-organization"
|
|
106
|
-
disabled={isSubmitting}
|
|
107
|
-
class="flex-1"
|
|
108
|
-
/>
|
|
109
|
-
<Button
|
|
110
|
-
type="button"
|
|
111
|
-
variant="outline"
|
|
112
|
-
size="sm"
|
|
113
|
-
onclick={() => (slug = generateSlug(name))}
|
|
114
|
-
disabled={!name.trim() || isSubmitting}
|
|
115
|
-
>
|
|
116
|
-
Generate
|
|
117
|
-
</Button>
|
|
118
|
-
</div>
|
|
119
|
-
<p class="text-muted-foreground mt-1 text-xs">
|
|
120
|
-
Lowercase letters, numbers, and hyphens only.
|
|
121
|
-
</p>
|
|
122
|
-
</div>
|
|
123
|
-
{#if error}
|
|
124
|
-
<p class="text-destructive text-sm">{error}</p>
|
|
125
|
-
{/if}
|
|
126
|
-
<Dialog.Footer>
|
|
127
|
-
<Button
|
|
128
|
-
type="button"
|
|
129
|
-
variant="outline"
|
|
130
|
-
onclick={() => (open = false)}
|
|
131
|
-
disabled={isSubmitting}
|
|
132
|
-
>
|
|
133
|
-
Cancel
|
|
134
|
-
</Button>
|
|
135
|
-
<Button type="submit" disabled={isSubmitting || !name.trim()}>
|
|
136
|
-
{isSubmitting ? 'Creating...' : 'Create'}
|
|
137
|
-
</Button>
|
|
138
|
-
</Dialog.Footer>
|
|
139
|
-
</form>
|
|
140
|
-
</Dialog.Content>
|
|
141
|
-
</Dialog.Root>
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { goto, invalidateAll } from '$app/navigation';
|
|
3
|
-
import * as AlertDialog from '@aphexcms/ui/shadcn/alert-dialog';
|
|
4
|
-
import { Badge } from '@aphexcms/ui/shadcn/badge';
|
|
5
|
-
import { ExternalLink } from '@lucide/svelte';
|
|
6
|
-
import { organizations } from '@aphexcms/cms-core/client';
|
|
7
|
-
|
|
8
|
-
type OrgItem = {
|
|
9
|
-
id: string;
|
|
10
|
-
name: string;
|
|
11
|
-
slug: string;
|
|
12
|
-
role: string;
|
|
13
|
-
isActive: boolean;
|
|
14
|
-
memberCount: number;
|
|
15
|
-
ownerEmail?: string;
|
|
16
|
-
};
|
|
17
|
-
|
|
18
|
-
let { orgs = [] }: { orgs: OrgItem[] } = $props();
|
|
19
|
-
|
|
20
|
-
let switchingOrgId = $state<string | null>(null);
|
|
21
|
-
|
|
22
|
-
async function handleOrgClick(org: OrgItem) {
|
|
23
|
-
if (switchingOrgId) return;
|
|
24
|
-
|
|
25
|
-
if (org.isActive) {
|
|
26
|
-
goto('/admin');
|
|
27
|
-
return;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
switchingOrgId = org.id;
|
|
31
|
-
try {
|
|
32
|
-
const result = await organizations.switch({ organizationId: org.id });
|
|
33
|
-
|
|
34
|
-
if (!result.success) {
|
|
35
|
-
throw new Error(result.error || 'Failed to switch organization');
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
await invalidateAll();
|
|
39
|
-
goto('/admin');
|
|
40
|
-
} catch (err) {
|
|
41
|
-
console.error('Failed to switch organization:', err);
|
|
42
|
-
} finally {
|
|
43
|
-
switchingOrgId = null;
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
function getInitials(name: string): string {
|
|
48
|
-
return name
|
|
49
|
-
.split(' ')
|
|
50
|
-
.map((word) => word[0])
|
|
51
|
-
.join('')
|
|
52
|
-
.toUpperCase()
|
|
53
|
-
.slice(0, 2);
|
|
54
|
-
}
|
|
55
|
-
</script>
|
|
56
|
-
|
|
57
|
-
{#if orgs.length === 0}
|
|
58
|
-
<p class="text-muted-foreground text-sm">No organizations yet</p>
|
|
59
|
-
{:else}
|
|
60
|
-
<div class="divide-y rounded-lg border">
|
|
61
|
-
{#each orgs as org (org.id)}
|
|
62
|
-
<div class="hover:bg-muted/50 flex items-center gap-4 p-4 transition-colors">
|
|
63
|
-
<!-- Avatar -->
|
|
64
|
-
<div
|
|
65
|
-
class="bg-sidebar-primary text-sidebar-primary-foreground flex size-10 shrink-0 items-center justify-center rounded-lg text-sm font-semibold"
|
|
66
|
-
>
|
|
67
|
-
{getInitials(org.name)}
|
|
68
|
-
</div>
|
|
69
|
-
|
|
70
|
-
<!-- Info -->
|
|
71
|
-
<div class="min-w-0 flex-1">
|
|
72
|
-
<div class="flex items-baseline gap-2">
|
|
73
|
-
<span class="font-medium">{org.name}</span>
|
|
74
|
-
<span class="text-muted-foreground">/</span>
|
|
75
|
-
<span class="text-muted-foreground text-sm">[{org.slug}]</span>
|
|
76
|
-
{#if org.isActive}
|
|
77
|
-
<Badge variant="default" class="text-xs">Active</Badge>
|
|
78
|
-
{/if}
|
|
79
|
-
</div>
|
|
80
|
-
<div class="text-muted-foreground text-sm">
|
|
81
|
-
{#if org.ownerEmail}
|
|
82
|
-
Owned by: {org.ownerEmail}
|
|
83
|
-
{/if}
|
|
84
|
-
</div>
|
|
85
|
-
<div class="text-muted-foreground text-sm">
|
|
86
|
-
Total members: {org.memberCount}
|
|
87
|
-
</div>
|
|
88
|
-
</div>
|
|
89
|
-
|
|
90
|
-
<!-- Actions -->
|
|
91
|
-
<button
|
|
92
|
-
class="text-muted-foreground hover:text-foreground hover:bg-muted rounded-md p-2 transition-colors"
|
|
93
|
-
onclick={() => handleOrgClick(org)}
|
|
94
|
-
disabled={switchingOrgId !== null}
|
|
95
|
-
title={org.isActive ? 'Go to dashboard' : 'Switch to this organization'}
|
|
96
|
-
>
|
|
97
|
-
<ExternalLink class="size-4" />
|
|
98
|
-
</button>
|
|
99
|
-
</div>
|
|
100
|
-
{/each}
|
|
101
|
-
</div>
|
|
102
|
-
{/if}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import type { LayoutServerLoad } from './$types';
|
|
2
|
-
import { organizationService } from '$lib/server/services/organization';
|
|
3
|
-
|
|
4
|
-
export const load: LayoutServerLoad = async ({ locals }) => {
|
|
5
|
-
const auth = locals.auth;
|
|
6
|
-
|
|
7
|
-
if (!auth || auth.type !== 'session') {
|
|
8
|
-
throw new Error('No session found');
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
const databaseAdapter = locals.aphexCMS.databaseAdapter;
|
|
12
|
-
let activeOrganization = null;
|
|
13
|
-
let currentUserOrgRole = null;
|
|
14
|
-
|
|
15
|
-
if (auth.organizationId) {
|
|
16
|
-
const orgData = await organizationService.getOrganizationWithMembers(auth.organizationId);
|
|
17
|
-
if (orgData) {
|
|
18
|
-
activeOrganization = {
|
|
19
|
-
...orgData.organization,
|
|
20
|
-
members: orgData.members.map((m) => ({
|
|
21
|
-
...m.member,
|
|
22
|
-
user: m.user,
|
|
23
|
-
invitedEmail: m.invitedEmail
|
|
24
|
-
}))
|
|
25
|
-
};
|
|
26
|
-
|
|
27
|
-
const currentMember = orgData.members.find((m) => m.user.id === auth.user.id);
|
|
28
|
-
currentUserOrgRole = currentMember?.member.role || null;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return {
|
|
33
|
-
user: {
|
|
34
|
-
id: auth.user.id,
|
|
35
|
-
email: auth.user.email,
|
|
36
|
-
name: auth.user.name,
|
|
37
|
-
role: auth.user.role,
|
|
38
|
-
organizationRole: currentUserOrgRole
|
|
39
|
-
},
|
|
40
|
-
activeOrganization
|
|
41
|
-
};
|
|
42
|
-
};
|
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { page } from '$app/state';
|
|
3
|
-
import type { Capability } from '@aphexcms/cms-core';
|
|
4
|
-
|
|
5
|
-
let { children } = $props();
|
|
6
|
-
|
|
7
|
-
const basePath = '/admin/settings';
|
|
8
|
-
|
|
9
|
-
// Resolve capabilities once — exposed by /admin/+layout.server.ts and
|
|
10
|
-
// inherited through the layout data chain.
|
|
11
|
-
const capabilities = $derived(page.data.rbac?.capabilities ?? []);
|
|
12
|
-
function has(cap: Capability): boolean {
|
|
13
|
-
return capabilities.includes(cap);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// `requires: null` means everyone with a session (e.g. Profile), `requires`
|
|
17
|
-
// with a capability hides the tab from users who don't have it. Members
|
|
18
|
-
// stays visible to anyone in an org since viewing who's in the org is not
|
|
19
|
-
// privileged information, but mutation controls on the page itself are
|
|
20
|
-
// already capability-gated server-side.
|
|
21
|
-
type Tab = { label: string; href: string; requires: Capability | null };
|
|
22
|
-
|
|
23
|
-
const orgTabs: Tab[] = $derived(
|
|
24
|
-
(
|
|
25
|
-
[
|
|
26
|
-
{ label: 'General', href: basePath, requires: 'org.settings' as Capability },
|
|
27
|
-
{ label: 'Members', href: `${basePath}/members`, requires: null },
|
|
28
|
-
{ label: 'Roles', href: `${basePath}/roles`, requires: 'role.manage' as Capability }
|
|
29
|
-
] satisfies Tab[]
|
|
30
|
-
).filter((t) => t.requires === null || has(t.requires))
|
|
31
|
-
);
|
|
32
|
-
|
|
33
|
-
const accountTabs: Tab[] = $derived(
|
|
34
|
-
(
|
|
35
|
-
[
|
|
36
|
-
{ label: 'Profile', href: `${basePath}/account`, requires: null },
|
|
37
|
-
{
|
|
38
|
-
label: 'API Keys',
|
|
39
|
-
href: `${basePath}/api-keys`,
|
|
40
|
-
requires: 'apiKey.manage' as Capability
|
|
41
|
-
}
|
|
42
|
-
] satisfies Tab[]
|
|
43
|
-
).filter((t) => t.requires === null || has(t.requires))
|
|
44
|
-
);
|
|
45
|
-
|
|
46
|
-
function isActive(href: string) {
|
|
47
|
-
if (href === basePath) return page.url.pathname === basePath;
|
|
48
|
-
return page.url.pathname.startsWith(href);
|
|
49
|
-
}
|
|
50
|
-
</script>
|
|
51
|
-
|
|
52
|
-
<div class="flex flex-1 flex-col gap-4 overflow-y-auto p-4 md:gap-8 md:p-8">
|
|
53
|
-
<div class="mx-auto grid w-full max-w-6xl gap-2">
|
|
54
|
-
<h1 class="text-3xl font-semibold">Settings</h1>
|
|
55
|
-
<p class="text-muted-foreground">Manage your organization and account</p>
|
|
56
|
-
</div>
|
|
57
|
-
|
|
58
|
-
<!-- Mobile tabs -->
|
|
59
|
-
<div class="mx-auto w-full max-w-6xl md:hidden">
|
|
60
|
-
<div class="border-b">
|
|
61
|
-
<div class="flex gap-4 overflow-x-auto">
|
|
62
|
-
{#each [...orgTabs, ...accountTabs] as tab}
|
|
63
|
-
<a
|
|
64
|
-
href={tab.href}
|
|
65
|
-
class="border-b-2 px-1 pb-2 text-sm font-medium whitespace-nowrap transition-colors {isActive(
|
|
66
|
-
tab.href
|
|
67
|
-
)
|
|
68
|
-
? 'border-primary text-primary'
|
|
69
|
-
: 'text-muted-foreground hover:text-foreground border-transparent'}"
|
|
70
|
-
>
|
|
71
|
-
{tab.label}
|
|
72
|
-
</a>
|
|
73
|
-
{/each}
|
|
74
|
-
</div>
|
|
75
|
-
</div>
|
|
76
|
-
</div>
|
|
77
|
-
|
|
78
|
-
<div
|
|
79
|
-
class="mx-auto grid w-full max-w-6xl items-start gap-6 md:grid-cols-[180px_1fr] lg:grid-cols-[250px_1fr]"
|
|
80
|
-
>
|
|
81
|
-
<!-- Desktop sidebar nav -->
|
|
82
|
-
<nav class="text-muted-foreground hidden gap-1 text-sm md:grid">
|
|
83
|
-
<p
|
|
84
|
-
class="text-muted-foreground/60 px-1 pt-2 pb-1 text-xs font-semibold tracking-wider uppercase"
|
|
85
|
-
>
|
|
86
|
-
Organization
|
|
87
|
-
</p>
|
|
88
|
-
{#each orgTabs as tab}
|
|
89
|
-
<a
|
|
90
|
-
href={tab.href}
|
|
91
|
-
class="rounded-md px-2 py-1.5 {isActive(tab.href)
|
|
92
|
-
? 'text-primary bg-muted font-semibold'
|
|
93
|
-
: ''}"
|
|
94
|
-
>
|
|
95
|
-
{tab.label}
|
|
96
|
-
</a>
|
|
97
|
-
{/each}
|
|
98
|
-
<p
|
|
99
|
-
class="text-muted-foreground/60 px-1 pt-4 pb-1 text-xs font-semibold tracking-wider uppercase"
|
|
100
|
-
>
|
|
101
|
-
Account
|
|
102
|
-
</p>
|
|
103
|
-
{#each accountTabs as tab}
|
|
104
|
-
<a
|
|
105
|
-
href={tab.href}
|
|
106
|
-
class="rounded-md px-2 py-1.5 {isActive(tab.href)
|
|
107
|
-
? 'text-primary bg-muted font-semibold'
|
|
108
|
-
: ''}"
|
|
109
|
-
>
|
|
110
|
-
{tab.label}
|
|
111
|
-
</a>
|
|
112
|
-
{/each}
|
|
113
|
-
</nav>
|
|
114
|
-
{@render children()}
|
|
115
|
-
</div>
|
|
116
|
-
</div>
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import type { PageServerLoad } from './$types';
|
|
2
|
-
import { redirect } from '@sveltejs/kit';
|
|
3
|
-
import { hasCapability } from '@aphexcms/cms-core';
|
|
4
|
-
|
|
5
|
-
export const load: PageServerLoad = async ({ locals }) => {
|
|
6
|
-
const auth = locals.auth;
|
|
7
|
-
|
|
8
|
-
// Users without org.settings can't do anything on the General tab — route
|
|
9
|
-
// them to their profile (always available) so the default settings URL
|
|
10
|
-
// doesn't land them on a locked page.
|
|
11
|
-
if (auth?.type === 'session' && !hasCapability(auth, 'org.settings')) {
|
|
12
|
-
throw redirect(302, '/admin/settings/account');
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
return {};
|
|
16
|
-
};
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import * as Card from '@aphexcms/ui/shadcn/card';
|
|
3
|
-
import type { PageData } from './$types';
|
|
4
|
-
import OrganizationsSettings from './_components/OrganizationsSettings.svelte';
|
|
5
|
-
|
|
6
|
-
let { data }: { data: PageData } = $props();
|
|
7
|
-
</script>
|
|
8
|
-
|
|
9
|
-
<svelte:head>
|
|
10
|
-
<title>Aphex CMS - Organization Settings</title>
|
|
11
|
-
</svelte:head>
|
|
12
|
-
|
|
13
|
-
<div class="grid gap-6">
|
|
14
|
-
<div class="hidden sm:block">
|
|
15
|
-
<h2 class="text-xl font-semibold">General</h2>
|
|
16
|
-
<p class="text-muted-foreground text-sm">Manage your organization settings and preferences.</p>
|
|
17
|
-
</div>
|
|
18
|
-
|
|
19
|
-
{#if data.activeOrganization}
|
|
20
|
-
<OrganizationsSettings
|
|
21
|
-
activeOrganization={data.activeOrganization}
|
|
22
|
-
currentUserId={data.user.id}
|
|
23
|
-
/>
|
|
24
|
-
{:else}
|
|
25
|
-
<Card.Root>
|
|
26
|
-
<Card.Content class="py-12 text-center">
|
|
27
|
-
<p class="text-muted-foreground text-lg">No active organization</p>
|
|
28
|
-
<p class="text-muted-foreground mt-2 text-sm">You need to be added to an organization</p>
|
|
29
|
-
</Card.Content>
|
|
30
|
-
</Card.Root>
|
|
31
|
-
{/if}
|
|
32
|
-
</div>
|
package/templates/base/src/routes/(protected)/admin/settings/_components/AccountSettings.svelte
DELETED
|
@@ -1,177 +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 { Switch } from '@aphexcms/ui/shadcn/switch';
|
|
7
|
-
import * as Card from '@aphexcms/ui/shadcn/card';
|
|
8
|
-
import * as Avatar from '@aphexcms/ui/shadcn/avatar';
|
|
9
|
-
import { Separator } from '@aphexcms/ui/shadcn/separator';
|
|
10
|
-
import { invalidateAll } from '$app/navigation';
|
|
11
|
-
import type { CMSUser, UserSessionPreferences } from '@aphexcms/cms-core';
|
|
12
|
-
import { user as userApi } from '@aphexcms/cms-core/client';
|
|
13
|
-
import { Building2, Lock } from '@lucide/svelte';
|
|
14
|
-
import { toast } from 'svelte-sonner';
|
|
15
|
-
|
|
16
|
-
type Props = {
|
|
17
|
-
user: CMSUser;
|
|
18
|
-
userPreferences?: UserSessionPreferences | null;
|
|
19
|
-
hasChildOrganizations?: boolean;
|
|
20
|
-
};
|
|
21
|
-
|
|
22
|
-
let { user, userPreferences = null, hasChildOrganizations = false }: Props = $props();
|
|
23
|
-
|
|
24
|
-
let userName = $state(user.name || '');
|
|
25
|
-
let isUpdating = $state(false);
|
|
26
|
-
let includeChildOrganizations = $state(userPreferences?.includeChildOrganizations ?? false);
|
|
27
|
-
let isUpdatingPreferences = $state(false);
|
|
28
|
-
|
|
29
|
-
const userInitials = $derived(
|
|
30
|
-
user.name
|
|
31
|
-
? user.name
|
|
32
|
-
.split(' ')
|
|
33
|
-
.map((w) => w[0])
|
|
34
|
-
.join('')
|
|
35
|
-
.toUpperCase()
|
|
36
|
-
.slice(0, 2)
|
|
37
|
-
: user.email[0].toUpperCase()
|
|
38
|
-
);
|
|
39
|
-
|
|
40
|
-
function getRoleBadgeVariant(role: string): 'default' | 'secondary' | 'outline' | 'destructive' {
|
|
41
|
-
switch (role) {
|
|
42
|
-
case 'super_admin':
|
|
43
|
-
return 'default';
|
|
44
|
-
case 'admin':
|
|
45
|
-
return 'secondary';
|
|
46
|
-
default:
|
|
47
|
-
return 'outline';
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function formatRole(role: string): string {
|
|
52
|
-
return role.replace(/_/g, ' ');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
async function updateProfile() {
|
|
56
|
-
if (!userName.trim()) {
|
|
57
|
-
toast.error('Please enter your name');
|
|
58
|
-
return;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
isUpdating = true;
|
|
62
|
-
try {
|
|
63
|
-
const result = await userApi.updateProfile({ name: userName.trim() });
|
|
64
|
-
|
|
65
|
-
if (!result.success) {
|
|
66
|
-
throw new Error(result.error || result.message || 'Failed to update profile');
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
toast.success('Profile updated successfully');
|
|
70
|
-
await invalidateAll();
|
|
71
|
-
} catch (error) {
|
|
72
|
-
toast.error(error instanceof Error ? error.message : 'Failed to update profile');
|
|
73
|
-
} finally {
|
|
74
|
-
isUpdating = false;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function updatePreferences(prefs: Partial<UserSessionPreferences>) {
|
|
79
|
-
isUpdatingPreferences = true;
|
|
80
|
-
try {
|
|
81
|
-
const result = await userApi.updatePreferences(prefs);
|
|
82
|
-
|
|
83
|
-
if (!result.success) {
|
|
84
|
-
throw new Error(result.error || result.message || 'Failed to update preferences');
|
|
85
|
-
}
|
|
86
|
-
} catch (error) {
|
|
87
|
-
toast.error(error instanceof Error ? error.message : 'Failed to update preferences');
|
|
88
|
-
// Revert on error
|
|
89
|
-
if (prefs.includeChildOrganizations !== undefined) {
|
|
90
|
-
includeChildOrganizations = !prefs.includeChildOrganizations;
|
|
91
|
-
}
|
|
92
|
-
} finally {
|
|
93
|
-
isUpdatingPreferences = false;
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
</script>
|
|
97
|
-
|
|
98
|
-
<div class="space-y-6">
|
|
99
|
-
<!-- Profile Information -->
|
|
100
|
-
<Card.Root>
|
|
101
|
-
<Card.Header>
|
|
102
|
-
<div class="flex items-center gap-4">
|
|
103
|
-
<Avatar.Root class="h-14 w-14 text-lg">
|
|
104
|
-
{#if user.image}
|
|
105
|
-
<Avatar.Image src={user.image} alt={user.name || user.email} />
|
|
106
|
-
{/if}
|
|
107
|
-
<Avatar.Fallback>{userInitials}</Avatar.Fallback>
|
|
108
|
-
</Avatar.Root>
|
|
109
|
-
<div class="min-w-0 flex-1">
|
|
110
|
-
<div class="flex items-center gap-2">
|
|
111
|
-
<Card.Title class="text-lg">{user.name || user.email}</Card.Title>
|
|
112
|
-
<Badge variant={getRoleBadgeVariant(user.role)} class="capitalize">
|
|
113
|
-
{formatRole(user.role)}
|
|
114
|
-
</Badge>
|
|
115
|
-
</div>
|
|
116
|
-
<p class="text-muted-foreground mt-0.5 text-sm">{user.email}</p>
|
|
117
|
-
</div>
|
|
118
|
-
</div>
|
|
119
|
-
</Card.Header>
|
|
120
|
-
|
|
121
|
-
<Card.Content>
|
|
122
|
-
<Separator class="mb-4" />
|
|
123
|
-
|
|
124
|
-
<div class="space-y-4">
|
|
125
|
-
<div>
|
|
126
|
-
<Label for="user-name">Display Name</Label>
|
|
127
|
-
<Input id="user-name" bind:value={userName} placeholder="Your name" class="mt-2" />
|
|
128
|
-
</div>
|
|
129
|
-
<div>
|
|
130
|
-
<Label for="user-email">Email</Label>
|
|
131
|
-
<div class="relative mt-1">
|
|
132
|
-
<Input id="user-email" type="email" value={user.email} disabled class="pr-9" />
|
|
133
|
-
<Lock
|
|
134
|
-
class="text-muted-foreground absolute top-1/2 right-3 h-3.5 w-3.5 -translate-y-1/2"
|
|
135
|
-
/>
|
|
136
|
-
</div>
|
|
137
|
-
<p class="text-muted-foreground mt-1 text-xs">Managed by your authentication provider</p>
|
|
138
|
-
</div>
|
|
139
|
-
</div>
|
|
140
|
-
</Card.Content>
|
|
141
|
-
<Card.Footer class="border-t px-6 py-4">
|
|
142
|
-
<Button onclick={updateProfile} disabled={isUpdating}>
|
|
143
|
-
{isUpdating ? 'Saving...' : 'Save Changes'}
|
|
144
|
-
</Button>
|
|
145
|
-
</Card.Footer>
|
|
146
|
-
</Card.Root>
|
|
147
|
-
|
|
148
|
-
<!-- Preferences -->
|
|
149
|
-
{#if hasChildOrganizations}
|
|
150
|
-
<Card.Root>
|
|
151
|
-
<Card.Header>
|
|
152
|
-
<Card.Title>Content Preferences</Card.Title>
|
|
153
|
-
</Card.Header>
|
|
154
|
-
<Card.Content>
|
|
155
|
-
<div class="flex items-center justify-between">
|
|
156
|
-
<div class="flex items-center gap-3">
|
|
157
|
-
<Building2 class="text-muted-foreground h-5 w-5" />
|
|
158
|
-
<div>
|
|
159
|
-
<Label class="text-base font-medium">Include child organizations</Label>
|
|
160
|
-
<p class="text-muted-foreground text-sm">
|
|
161
|
-
Show documents from child organizations in your content lists
|
|
162
|
-
</p>
|
|
163
|
-
</div>
|
|
164
|
-
</div>
|
|
165
|
-
<Switch
|
|
166
|
-
checked={includeChildOrganizations}
|
|
167
|
-
disabled={isUpdatingPreferences}
|
|
168
|
-
onCheckedChange={(checked) => {
|
|
169
|
-
includeChildOrganizations = checked;
|
|
170
|
-
updatePreferences({ includeChildOrganizations: checked });
|
|
171
|
-
}}
|
|
172
|
-
/>
|
|
173
|
-
</div>
|
|
174
|
-
</Card.Content>
|
|
175
|
-
</Card.Root>
|
|
176
|
-
{/if}
|
|
177
|
-
</div>
|