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,26 +0,0 @@
1
- <script lang="ts">
2
- import { Heading, Text, Button, Section } from 'better-svelte-email';
3
- import EmailLayout from './EmailLayout.svelte';
4
-
5
- let { userName, verifyUrl }: { userName: string; verifyUrl: string } = $props();
6
- </script>
7
-
8
- <EmailLayout preview="Verify your email address">
9
- <Heading as="h1" class="text-[24px] font-bold text-[#1f2937]">Email Verification</Heading>
10
-
11
- <Text class="text-[16px] text-[#374151]">Hi {userName},</Text>
12
-
13
- <Text class="text-[16px] text-[#374151]">
14
- Welcome to Aphex CMS! Please verify your email address by clicking the button below:
15
- </Text>
16
-
17
- <Section class="my-[32px] text-center">
18
- <Button href={verifyUrl} pX={20} pY={12} class="rounded-[4px] bg-[#1f2937] text-white">
19
- Verify Email
20
- </Button>
21
- </Section>
22
-
23
- <Text class="text-[14px] text-[#6b7280]">
24
- If you didn't create this account, you can safely ignore this email.
25
- </Text>
26
- </EmailLayout>
@@ -1,30 +0,0 @@
1
- <script lang="ts">
2
- import { Heading, Text, Button, Section } from 'better-svelte-email';
3
- import EmailLayout from './EmailLayout.svelte';
4
-
5
- let { orgName, role, inviteUrl }: { orgName: string; role: string; inviteUrl: string } = $props();
6
- const article = role === 'admin' ? 'an' : 'a';
7
- </script>
8
-
9
- <EmailLayout preview="You've been invited to join {orgName}">
10
- <Heading as="h1" class="text-[24px] font-bold text-[#1f2937]">Organization Invitation</Heading>
11
-
12
- <Text class="text-[16px] text-[#374151]">
13
- You've been invited to join <strong>{orgName}</strong> as {article}
14
- <strong>{role}</strong>.
15
- </Text>
16
-
17
- <Text class="text-[16px] text-[#374151]">Click the button below to accept the invitation:</Text>
18
-
19
- <Section class="my-[32px] text-center">
20
- <Button href={inviteUrl} pX={20} pY={12} class="rounded-[4px] bg-[#1f2937] text-white">
21
- Accept Invitation
22
- </Button>
23
- </Section>
24
-
25
- <Text class="text-[14px] text-[#6b7280]">This invitation will expire in 7 days.</Text>
26
-
27
- <Text class="text-[14px] text-[#6b7280]">
28
- If you didn't expect this invitation, you can safely ignore this email.
29
- </Text>
30
- </EmailLayout>
@@ -1,28 +0,0 @@
1
- <script lang="ts">
2
- import { Heading, Text, Button, Section } from 'better-svelte-email';
3
- import EmailLayout from './EmailLayout.svelte';
4
-
5
- let { userName, resetUrl }: { userName: string; resetUrl: string } = $props();
6
- </script>
7
-
8
- <EmailLayout preview="Reset your password">
9
- <Heading as="h1" class="text-[24px] font-bold text-[#1f2937]">Password Reset</Heading>
10
-
11
- <Text class="text-[16px] text-[#374151]">Hi {userName},</Text>
12
-
13
- <Text class="text-[16px] text-[#374151]">
14
- You requested to reset your password. Click the button below to reset it:
15
- </Text>
16
-
17
- <Section class="my-[32px] text-center">
18
- <Button href={resetUrl} pX={20} pY={12} class="rounded-[4px] bg-[#1f2937] text-white">
19
- Reset Password
20
- </Button>
21
- </Section>
22
-
23
- <Text class="text-[14px] text-[#6b7280]">
24
- If you didn't request this, you can safely ignore this email.
25
- </Text>
26
-
27
- <Text class="text-[14px] text-[#6b7280]">This link will expire in 1 hour.</Text>
28
- </EmailLayout>
@@ -1,28 +0,0 @@
1
- import { createMailpitAdapter } from '@aphexcms/nodemailer-adapter';
2
- import { createResendAdapter } from '@aphexcms/resend-adapter';
3
- import { env } from '$env/dynamic/private';
4
- import { dev } from '$app/environment';
5
- import { cmsLogger } from '@aphexcms/cms-core';
6
- import { passwordReset } from './templates/password-reset';
7
- import { emailVerification } from './templates/email-verification';
8
- import { invitation } from './templates/invitation';
9
-
10
- export const email = dev
11
- ? createMailpitAdapter()
12
- : createResendAdapter({ apiKey: env.RESEND_API_KEY ?? '' });
13
-
14
- if (dev) {
15
- cmsLogger.info('[Email]', 'Using Mailpit adapter (dev mode)');
16
- cmsLogger.info('[Email]', 'View emails at http://localhost:8025');
17
- } else {
18
- cmsLogger.info('[Email]', 'Using Resend adapter (production)');
19
- }
20
-
21
- export const emailConfig = {
22
- from: 'Ben @ Aphex CMS <ben@newsletter.getaphex.com>',
23
- passwordReset,
24
- emailVerification,
25
- invitation
26
- };
27
-
28
- export type AuthEmailConfig = typeof emailConfig;
@@ -1,56 +0,0 @@
1
- import type { Hono } from 'hono';
2
- import type { AphexEnv } from '@aphexcms/cms-core/server';
3
- import { email, emailConfig } from './index';
4
-
5
- /**
6
- * Wrap the built-in `/api/organizations/invitations` POST so a successful
7
- * invite also dispatches the email. Email send is fire-and-forget — never
8
- * blocks the response, never fails the invite.
9
- */
10
- export function registerInvitationEmailHook(app: Hono<AphexEnv>) {
11
- app.use('/organizations/invitations', async (c, next) => {
12
- if (c.req.method !== 'POST') return next();
13
-
14
- const reqClone = c.req.raw.clone();
15
- await next();
16
- if (c.res.status !== 201) return;
17
-
18
- try {
19
- const body = await reqClone.json();
20
- const result = await c.res.clone().json();
21
- const invitation = result.data;
22
- if (!invitation?.token) return;
23
-
24
- const auth = c.var.auth;
25
- const { databaseAdapter } = c.var.aphexCMS;
26
- const org =
27
- auth && auth.type !== 'partial_session'
28
- ? await databaseAdapter.findOrganizationById(auth.organizationId)
29
- : null;
30
- const orgName = org?.name || 'an organization';
31
- const inviteUrl = `${new URL(c.req.url).origin}/invite/${invitation.token}`;
32
-
33
- void (async () => {
34
- try {
35
- const { html, text } = await emailConfig.invitation.render(
36
- orgName,
37
- body.role,
38
- inviteUrl
39
- );
40
- await email.send({
41
- from: emailConfig.from,
42
- to: body.email.toLowerCase(),
43
- subject: emailConfig.invitation.getSubject(orgName),
44
- html,
45
- text
46
- });
47
- console.log(`[Invitations]: Invitation email sent to ${body.email}`);
48
- } catch (err) {
49
- console.error(`[Invitations]: Failed to send invitation email to ${body.email}:`, err);
50
- }
51
- })();
52
- } catch (err) {
53
- console.error('[Invitations]: Failed to send invitation email:', err);
54
- }
55
- });
56
- }
@@ -1,13 +0,0 @@
1
- import Renderer, { toPlainText } from 'better-svelte-email/render';
2
- import type { Component } from 'svelte';
3
-
4
- const { render } = new Renderer();
5
-
6
- export async function renderEmail<Props extends Record<string, unknown>>(
7
- component: Component<Props>,
8
- props: Props
9
- ): Promise<{ html: string; text: string }> {
10
- const html = await render(component, { props });
11
- const text = toPlainText(html);
12
- return { html, text };
13
- }
@@ -1,8 +0,0 @@
1
- import { renderEmail } from '../renderer';
2
- import EmailVerification from '../components/EmailVerification.svelte';
3
-
4
- export const emailVerification = {
5
- subject: 'Verify your email address',
6
- render: (userName: string, verifyUrl: string) =>
7
- renderEmail(EmailVerification, { userName, verifyUrl })
8
- };
@@ -1,8 +0,0 @@
1
- import { renderEmail } from '../renderer';
2
- import Invitation from '../components/Invitation.svelte';
3
-
4
- export const invitation = {
5
- getSubject: (orgName: string) => `You've been invited to join ${orgName}`,
6
- render: (orgName: string, role: string, inviteUrl: string) =>
7
- renderEmail(Invitation, { orgName, role, inviteUrl })
8
- };
@@ -1,7 +0,0 @@
1
- import { renderEmail } from '../renderer';
2
- import PasswordReset from '../components/PasswordReset.svelte';
3
-
4
- export const passwordReset = {
5
- subject: 'Reset your password',
6
- render: (userName: string, resetUrl: string) => renderEmail(PasswordReset, { userName, resetUrl })
7
- };
@@ -1,68 +0,0 @@
1
- // Organization service - app-specific cross-domain queries
2
- // Handles joins between auth (user) and CMS (organizations) tables
3
- import { drizzleDb } from '../db';
4
- import { user } from '../db/auth-schema';
5
- import { organizationMembers, organizations, invitations } from '../db/cms-schema';
6
- import { eq } from 'drizzle-orm';
7
- import type { Organization, OrganizationMemberWithUser } from '@aphexcms/cms-core';
8
-
9
- export interface OrganizationMemberWithUserAndInvite extends OrganizationMemberWithUser {
10
- invitedEmail?: string | null;
11
- }
12
-
13
- export interface OrganizationWithMembers {
14
- organization: Organization;
15
- members: OrganizationMemberWithUserAndInvite[];
16
- }
17
-
18
- export const organizationService = {
19
- /**
20
- * Get organization with enriched member data (includes user details)
21
- * This performs a join between CMS organizationMembers and auth user tables
22
- */
23
- async getOrganizationWithMembers(
24
- organizationId: string
25
- ): Promise<OrganizationWithMembers | null> {
26
- // Fetch organization
27
- const org = await drizzleDb.query.organizations.findFirst({
28
- where: eq(organizations.id, organizationId)
29
- });
30
-
31
- if (!org) {
32
- return null;
33
- }
34
-
35
- // Fetch members with user details and invitation data using joins
36
- const members = await drizzleDb
37
- .select({
38
- member: organizationMembers,
39
- user: {
40
- id: user.id,
41
- email: user.email,
42
- name: user.name,
43
- image: user.image
44
- },
45
- invitation: {
46
- email: invitations.email
47
- }
48
- })
49
- .from(organizationMembers)
50
- .innerJoin(user, eq(organizationMembers.userId, user.id))
51
- .leftJoin(invitations, eq(organizationMembers.invitationId, invitations.id))
52
- .where(eq(organizationMembers.organizationId, organizationId));
53
-
54
- return {
55
- organization: org,
56
- members: members.map((m) => ({
57
- member: m.member,
58
- user: {
59
- id: m.user.id,
60
- email: m.user.email,
61
- name: m.user.name,
62
- image: m.user.image
63
- },
64
- invitedEmail: m.invitation?.email || null
65
- }))
66
- };
67
- }
68
- };
@@ -1,28 +0,0 @@
1
- // apps/studio/src/lib/server/storage/index.ts
2
- import { s3Storage } from '@aphexcms/storage-s3';
3
- import { createStorageAdapter } from '@aphexcms/cms-core/server';
4
- import { env } from '$env/dynamic/private';
5
- import type { StorageAdapter } from '@aphexcms/cms-core/server';
6
-
7
- let storageAdapter: StorageAdapter;
8
-
9
- // Check for R2/S3 environment variables
10
- if (env.R2_BUCKET && env.R2_ENDPOINT && env.R2_ACCESS_KEY_ID && env.R2_SECRET_ACCESS_KEY) {
11
- // If all R2/S3 variables are present, create an S3 storage adapter.
12
- storageAdapter = s3Storage({
13
- bucket: env.R2_BUCKET,
14
- endpoint: env.R2_ENDPOINT,
15
- accessKeyId: env.R2_ACCESS_KEY_ID,
16
- secretAccessKey: env.R2_SECRET_ACCESS_KEY,
17
- publicUrl: env.R2_PUBLIC_URL || ''
18
- }).adapter;
19
- } else {
20
- // Otherwise, default to local filesystem storage
21
- storageAdapter = createStorageAdapter('local', {
22
- basePath: './static/uploads',
23
- baseUrl: '/uploads'
24
- });
25
- }
26
-
27
- // Export the single, shared storage adapter instance
28
- export { storageAdapter };
@@ -1,14 +0,0 @@
1
- function createActiveTab() {
2
- let activeTab = $state<'structure' | 'vision' | 'media'>('structure');
3
-
4
- return {
5
- get value() {
6
- return activeTab;
7
- },
8
- set value(val: 'structure' | 'vision' | 'media') {
9
- activeTab = val;
10
- }
11
- };
12
- }
13
-
14
- export const activeTabState = createActiveTab();
@@ -1,3 +0,0 @@
1
- import { imageUrlBuilder } from '@aphexcms/cms-core';
2
-
3
- export const urlFor = imageUrlBuilder();
@@ -1,33 +0,0 @@
1
- /**
2
- * Generate a URL-friendly slug from a string
3
- * @param text - The text to convert to a slug
4
- * @returns A URL-safe slug string
5
- */
6
- export function generateSlug(text: string): string {
7
- if (!text) return '';
8
-
9
- return (
10
- text
11
- .toString()
12
- .toLowerCase()
13
- .trim()
14
- // Replace spaces and multiple special characters with single hyphens
15
- .replace(/[^a-z0-9]+/g, '-')
16
- // Remove leading and trailing hyphens
17
- .replace(/^-+|-+$/g, '')
18
- );
19
- }
20
-
21
- /**
22
- * Validate if a string is a valid slug
23
- * @param slug - The slug to validate
24
- * @returns Boolean indicating if the slug is valid
25
- */
26
- export function isValidSlug(slug: string): boolean {
27
- if (!slug) return false;
28
-
29
- // Valid slug: lowercase letters, numbers, and hyphens only
30
- // Cannot start or end with hyphen
31
- const slugPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
32
- return slugPattern.test(slug);
33
- }
@@ -1,86 +0,0 @@
1
- import type { LayoutServerLoad } from './$types';
2
- import type { SidebarData, SidebarOrganization } from '@aphexcms/cms-core';
3
- import { redirect } from '@sveltejs/kit';
4
- import cmsConfig from '../../../../aphex.config';
5
-
6
- export const load: LayoutServerLoad = async ({ locals, url }) => {
7
- try {
8
- // User is guaranteed to exist here because /admin is protected by auth hook
9
- const auth = locals.auth;
10
-
11
- if (!auth || auth.type !== 'session') {
12
- console.error('[Layout Load] No session found');
13
- throw new Error('No session found');
14
- }
15
-
16
- // Fetch user's organizations directly from database (only once per page load)
17
- const db = locals.aphexCMS.databaseAdapter;
18
- const userOrgMemberships = await db.findUserOrganizations(auth.user.id);
19
-
20
- // If user has no orgs and isn't a super_admin, redirect to invitations page
21
- if (userOrgMemberships.length === 0 && auth.user.role !== 'super_admin') {
22
- throw redirect(302, '/invitations');
23
- }
24
-
25
- // Load instance settings to determine org creation permissions
26
- const instanceSettings = await db.getInstanceSettings();
27
-
28
- // Map to sidebar format
29
- const organizations: SidebarOrganization[] = userOrgMemberships.map((membership) => ({
30
- id: membership.organization.id,
31
- name: membership.organization.name,
32
- slug: membership.organization.slug,
33
- role: membership.member.role,
34
- isActive: membership.organization.id === auth.organizationId,
35
- metadata: membership.organization.metadata
36
- }));
37
-
38
- const activeOrganization = organizations.find((org) => org.isActive);
39
-
40
- // Determine if user can create organizations from admin panel
41
- // Super admins can always create (from god-mode), but from admin panel
42
- // it depends on the instance setting
43
- const canCreateOrganization =
44
- auth.user.role === 'super_admin' || (instanceSettings.allowUserOrgCreation ?? false);
45
-
46
- // Prepare sidebar data
47
- const sidebarData: SidebarData = {
48
- user: {
49
- id: auth.user.id,
50
- email: auth.user.email,
51
- name: auth.user.name,
52
- image: auth.user.image,
53
- role: auth.user.role
54
- },
55
- branding: {
56
- title: cmsConfig.customization?.branding?.title || 'Aphex CMS'
57
- },
58
- // Default nav items (can be customized per app)
59
- navItems: [
60
- { href: '/admin', label: 'Studio' }
61
- // Apps can add more: Settings, Media, etc.
62
- ],
63
- organizations,
64
- activeOrganization,
65
- canCreateOrganization
66
- };
67
-
68
- console.log(
69
- `[RBAC] user=${auth.user.id} org=${auth.organizationId} role="${auth.organizationRole}" caps=[${(auth.capabilities ?? []).join(',')}]`
70
- );
71
-
72
- return {
73
- sidebarData,
74
- // Expose resolved capabilities + active role to the admin shell so
75
- // client code (UI gating, debug panels) can consult the same set the
76
- // server enforces against.
77
- rbac: {
78
- role: auth.organizationRole,
79
- capabilities: auth.capabilities ?? []
80
- }
81
- };
82
- } catch (error) {
83
- console.error('[Layout Load] Error:', error);
84
- throw error;
85
- }
86
- };
@@ -1,78 +0,0 @@
1
- <script lang="ts">
2
- import {
3
- Sidebar,
4
- ConfirmDialogHost,
5
- PermissionsDebug,
6
- setPermissionsContext
7
- } from '@aphexcms/cms-core/client';
8
- import { authClient } from '$lib/auth-client';
9
- import { goto } from '$app/navigation';
10
- import { resolve } from '$app/paths';
11
- import { page } from '$app/state';
12
- import { activeTabState } from '$lib/stores/activeTab.svelte';
13
- import { SvelteURLSearchParams } from 'svelte/reactivity';
14
- import type { LayoutData } from './$types';
15
-
16
- let { data, children }: { data: LayoutData; children: any } = $props();
17
-
18
- // Publish capability context for every descendant route (AdminApp, settings
19
- // pages, etc). Sourced from the layout's rbac payload so capability edits
20
- // mid-session propagate on next page load. The role name is exposed too
21
- // for role-list checks (field-level access).
22
- setPermissionsContext(
23
- () => page.data.rbac?.capabilities ?? [],
24
- () => page.data.rbac?.role ?? null
25
- );
26
-
27
- // Get graphqlSettings from page data (child route)
28
- const enableGraphiQL = $derived(page.data.graphqlSettings?.enableGraphiQL ?? false);
29
-
30
- // Restore tab from URL param on load
31
- const validViews = ['structure', 'vision', 'media'] as const;
32
- type ViewType = (typeof validViews)[number];
33
-
34
- $effect(() => {
35
- const viewParam = page.url.searchParams.get('view');
36
- if (viewParam && validViews.includes(viewParam as ViewType)) {
37
- activeTabState.value = viewParam as ViewType;
38
- } else {
39
- activeTabState.value = 'structure';
40
- }
41
- });
42
-
43
- // Handle tab change — update both state and URL
44
- function handleTabChange(value: string) {
45
- activeTabState.value = value as ViewType;
46
-
47
- const params = new SvelteURLSearchParams(page.url.searchParams);
48
- if (value === 'structure') {
49
- params.delete('view');
50
- } else {
51
- params.set('view', value);
52
- }
53
- const query = params.toString();
54
- goto(`/admin${query ? `?${query}` : ''}`, { replaceState: true, keepFocus: true });
55
- }
56
-
57
- async function handleSignOut() {
58
- await authClient.signOut();
59
- goto(resolve('/login'));
60
- }
61
- </script>
62
-
63
- {#if data?.sidebarData}
64
- <Sidebar
65
- data={data.sidebarData}
66
- onSignOut={handleSignOut}
67
- {enableGraphiQL}
68
- activeTab={activeTabState}
69
- onTabChange={handleTabChange}
70
- >
71
- {@render children()}
72
- </Sidebar>
73
- {:else}
74
- <div>Loading...</div>
75
- {/if}
76
-
77
- <ConfirmDialogHost />
78
- <!-- <PermissionsDebug /> -->
@@ -1,60 +0,0 @@
1
- import { isViewer } from '@aphexcms/cms-core/server';
2
- import { redirect } from '@sveltejs/kit';
3
-
4
- export async function load({ locals }) {
5
- try {
6
- const { cmsEngine, databaseAdapter } = locals.aphexCMS;
7
- const auth = locals.auth;
8
-
9
- if (!auth) {
10
- redirect(307, '/login');
11
- }
12
-
13
- // Check for schema validation errors
14
- const schemaError = (locals.aphexCMS as any).schemaError;
15
- if (schemaError) {
16
- return {
17
- documentTypes: [],
18
- schemaError: {
19
- message: schemaError.message
20
- },
21
- graphqlSettings: null,
22
- isReadOnly: false,
23
- userPreferences: null
24
- };
25
- }
26
-
27
- const documentTypes = await cmsEngine.listDocumentTypes();
28
- // Fetch user profile preferences
29
- const userProfile = await databaseAdapter.findUserProfileById(
30
- auth.type == 'session' ? auth.user.id : ''
31
- );
32
- const userPreferences = userProfile?.preferences || {};
33
-
34
- // Get GraphQL settings (built-in, enabled by default)
35
- const graphqlSettings = locals.aphexCMS?.graphqlSettings ?? null;
36
-
37
- // Compute read-only access based on organization role
38
- const isReadOnly = auth?.type === 'session' ? isViewer(auth) : false;
39
-
40
- return {
41
- documentTypes,
42
- schemaError: null,
43
- graphqlSettings,
44
- isReadOnly,
45
- userPreferences
46
- };
47
- } catch (error) {
48
- console.error('Failed to load schema types:', error);
49
-
50
- return {
51
- documentTypes: [],
52
- schemaError: {
53
- message: error instanceof Error ? error.message : 'Unknown schema error'
54
- },
55
- graphqlSettings: null,
56
- isReadOnly: false,
57
- userPreferences: null
58
- };
59
- }
60
- }
@@ -1,34 +0,0 @@
1
- <script lang="ts">
2
- import { AdminApp } from '@aphexcms/cms-core/client';
3
- import { schemaTypes } from '$lib/schemaTypes/index';
4
- import { activeTabState } from '$lib/stores/activeTab.svelte';
5
- import { page } from '$app/state';
6
-
7
- let { data } = $props();
8
-
9
- // Capabilities + role come from the parent admin layout's rbac payload so
10
- // AdminApp (and its descendants) can gate individual actions per-capability
11
- // and apply field-level role-list access.
12
- const capabilities = $derived(page.data.rbac?.capabilities ?? []);
13
- const rbacRole = $derived(page.data.rbac?.role ?? null);
14
-
15
- // Tab change is handled by the layout's onTabChange callback
16
- // which syncs both activeTabState and URL params
17
- function handleTabChange(value: string) {
18
- if (activeTabState) activeTabState.value = value as 'structure' | 'vision' | 'media';
19
- }
20
- </script>
21
-
22
- <AdminApp
23
- schemas={schemaTypes}
24
- documentTypes={data.documentTypes}
25
- schemaError={data.schemaError}
26
- graphqlSettings={data.graphqlSettings}
27
- isReadOnly={data.isReadOnly}
28
- {capabilities}
29
- {rbacRole}
30
- userPreferences={data.userPreferences}
31
- activeTab={activeTabState}
32
- {handleTabChange}
33
- title="Aphex CMS"
34
- />
@@ -1,39 +0,0 @@
1
- import type { PageServerLoad } from './$types';
2
- import { authService } from '$lib/server/auth/service';
3
-
4
- export const load: PageServerLoad = async ({ locals }) => {
5
- const { databaseAdapter } = locals.aphexCMS;
6
- const auth = locals.auth;
7
-
8
- if (!auth || auth.type !== 'session') {
9
- return { organizations: [] };
10
- }
11
-
12
- const memberships = await databaseAdapter.findUserOrganizations(auth.user.id);
13
-
14
- const orgs = await Promise.all(
15
- memberships.map(async (membership) => {
16
- const members = await databaseAdapter.findOrganizationMembers(membership.organization.id);
17
- const owner = members.find((m) => m.role === 'owner');
18
-
19
- // Resolve owner email from auth system
20
- let ownerEmail: string | undefined;
21
- if (owner) {
22
- const ownerUser = await authService.getUserById(owner.userId);
23
- ownerEmail = ownerUser?.email;
24
- }
25
-
26
- return {
27
- id: membership.organization.id,
28
- name: membership.organization.name,
29
- slug: membership.organization.slug,
30
- role: membership.member.role,
31
- isActive: membership.organization.id === auth.organizationId,
32
- memberCount: members.length,
33
- ownerEmail
34
- };
35
- })
36
- );
37
-
38
- return { organizations: orgs };
39
- };