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,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,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
|
-
};
|