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,486 +0,0 @@
|
|
|
1
|
-
import { auth } from './instance.js';
|
|
2
|
-
import { apikey, user } from '../db/auth-schema';
|
|
3
|
-
import { drizzleDb } from '../db';
|
|
4
|
-
import { eq } from 'drizzle-orm';
|
|
5
|
-
import type {
|
|
6
|
-
SessionAuth,
|
|
7
|
-
PartialSessionAuth,
|
|
8
|
-
ApiKeyAuth,
|
|
9
|
-
CMSUser,
|
|
10
|
-
NewUserProfileData,
|
|
11
|
-
DatabaseAdapter
|
|
12
|
-
} from '@aphexcms/cms-core/server';
|
|
13
|
-
import { AuthError } from '@aphexcms/cms-core/server';
|
|
14
|
-
import { cmsLogger } from '@aphexcms/cms-core';
|
|
15
|
-
|
|
16
|
-
// This is the new AuthService that centralizes all auth-related server operations.
|
|
17
|
-
// It uses dependency injection for the DatabaseAdapter, making it more testable and decoupled.
|
|
18
|
-
|
|
19
|
-
export interface ApiKey {
|
|
20
|
-
id: string;
|
|
21
|
-
name: string | null;
|
|
22
|
-
organizationId?: string;
|
|
23
|
-
[key: string]: any;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface ApiKeyWithSecret extends ApiKey {
|
|
27
|
-
key: string;
|
|
28
|
-
organizationId: string; // Make required (override optional from ApiKey)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export interface CreateApiKeyData {
|
|
32
|
-
name: string;
|
|
33
|
-
/**
|
|
34
|
-
* Legacy coarse-grained scopes. Optional when `capabilities` is supplied —
|
|
35
|
-
* at least one of the two must be present (validator enforces this).
|
|
36
|
-
*/
|
|
37
|
-
permissions?: string[];
|
|
38
|
-
/** Fine-grained capability allowlist for this key. */
|
|
39
|
-
capabilities?: string[];
|
|
40
|
-
expiresInDays?: number;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export interface AuthService {
|
|
44
|
-
getSession(
|
|
45
|
-
request: Request,
|
|
46
|
-
db: DatabaseAdapter
|
|
47
|
-
): Promise<SessionAuth | PartialSessionAuth | null>;
|
|
48
|
-
requireSession(request: Request, db: DatabaseAdapter): Promise<SessionAuth>;
|
|
49
|
-
validateApiKey(request: Request): Promise<ApiKeyAuth | null>;
|
|
50
|
-
requireApiKey(
|
|
51
|
-
request: Request,
|
|
52
|
-
db: DatabaseAdapter,
|
|
53
|
-
permission?: 'read' | 'write'
|
|
54
|
-
): Promise<ApiKeyAuth>;
|
|
55
|
-
listApiKeys(db: DatabaseAdapter, userId: string): Promise<ApiKey[]>;
|
|
56
|
-
createApiKey(
|
|
57
|
-
userId: string,
|
|
58
|
-
organizationId: string,
|
|
59
|
-
data: CreateApiKeyData
|
|
60
|
-
): Promise<ApiKeyWithSecret>;
|
|
61
|
-
deleteApiKey(userId: string, keyId: string): Promise<boolean>;
|
|
62
|
-
getUserById(userId: string): Promise<{ id: string; name?: string; email: string } | null>;
|
|
63
|
-
getUserByEmail(email: string): Promise<{ id: string; name?: string; email: string } | null>;
|
|
64
|
-
changeUserName(userId: string, name: string): Promise<void>;
|
|
65
|
-
requestPasswordReset(email: string, redirectTo?: string): Promise<void>;
|
|
66
|
-
resetPassword(token: string, newPassword: string): Promise<void>;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
export const authService: AuthService = {
|
|
70
|
-
async getSession(
|
|
71
|
-
request: Request,
|
|
72
|
-
db: DatabaseAdapter
|
|
73
|
-
): Promise<SessionAuth | PartialSessionAuth | null> {
|
|
74
|
-
try {
|
|
75
|
-
cmsLogger.debug('[AuthService]', 'getSession called');
|
|
76
|
-
// 1. Get the base user session from the auth provider
|
|
77
|
-
const session = await auth.api.getSession({ headers: request.headers });
|
|
78
|
-
if (!session) {
|
|
79
|
-
cmsLogger.debug('[AuthService]', 'No active session found from auth provider');
|
|
80
|
-
return null;
|
|
81
|
-
}
|
|
82
|
-
cmsLogger.debug('[AuthService]', `Found session for user ${session.user.id}`);
|
|
83
|
-
|
|
84
|
-
// 2. Get the corresponding CMS user profile from our database
|
|
85
|
-
cmsLogger.debug('[AuthService]', `Checking for user profile for ${session.user.id}`);
|
|
86
|
-
let userProfile = await db.findUserProfileById(session.user.id);
|
|
87
|
-
|
|
88
|
-
// 3. If no profile exists, create one (lazy sync)
|
|
89
|
-
if (!userProfile) {
|
|
90
|
-
cmsLogger.info(
|
|
91
|
-
'[AuthService]',
|
|
92
|
-
`User profile not found for ${session.user.id}. Creating one now (lazy sync).`
|
|
93
|
-
);
|
|
94
|
-
|
|
95
|
-
// Check if this is the first user in the system
|
|
96
|
-
const hasExistingUsers =
|
|
97
|
-
typeof db.hasAnyUserProfiles === 'function' ? await db.hasAnyUserProfiles() : false;
|
|
98
|
-
const isFirstUser = !hasExistingUsers;
|
|
99
|
-
|
|
100
|
-
const newUserProfile: NewUserProfileData = {
|
|
101
|
-
userId: session.user.id,
|
|
102
|
-
role: isFirstUser ? 'super_admin' : 'editor' // First user gets super_admin, others get editor
|
|
103
|
-
};
|
|
104
|
-
userProfile = await db.createUserProfile(newUserProfile);
|
|
105
|
-
cmsLogger.info(
|
|
106
|
-
'[AuthService]',
|
|
107
|
-
`Successfully created user profile for ${session.user.id}${isFirstUser ? ' with SUPER_ADMIN role' : ''}`
|
|
108
|
-
);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
// 4. Combine the two into the final CMSUser object
|
|
112
|
-
const cmsUser: CMSUser = {
|
|
113
|
-
id: session.user.id,
|
|
114
|
-
email: session.user.email,
|
|
115
|
-
name: session.user.name ?? undefined,
|
|
116
|
-
image: session.user.image ?? undefined,
|
|
117
|
-
role: userProfile.role,
|
|
118
|
-
preferences: userProfile.preferences
|
|
119
|
-
};
|
|
120
|
-
cmsLogger.debug('[AuthService]', `Successfully assembled CMSUser for ${session.user.id}`);
|
|
121
|
-
|
|
122
|
-
// 5. Get the user's active organization from their session
|
|
123
|
-
cmsLogger.debug('[AuthService]', `Fetching active organization for ${session.user.id}`);
|
|
124
|
-
const userSession = await db.findUserSession(session.user.id);
|
|
125
|
-
|
|
126
|
-
// If no session exists, get the user's first organization as the default
|
|
127
|
-
if (!userSession) {
|
|
128
|
-
cmsLogger.debug('[AuthService]', `No user session found. Fetching user's organizations.`);
|
|
129
|
-
const userOrgs = await db.findUserOrganizations(session.user.id);
|
|
130
|
-
|
|
131
|
-
if (userOrgs.length === 0) {
|
|
132
|
-
// If this is a super_admin with no orgs, create a default organization
|
|
133
|
-
if (cmsUser.role === 'super_admin') {
|
|
134
|
-
cmsLogger.info(
|
|
135
|
-
'[AuthService]',
|
|
136
|
-
`Super admin ${session.user.id} has no organizations. Creating default organization.`
|
|
137
|
-
);
|
|
138
|
-
|
|
139
|
-
const defaultOrg = await db.createOrganization({
|
|
140
|
-
name: 'Default Organization',
|
|
141
|
-
slug: 'default',
|
|
142
|
-
createdBy: session.user.id
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
// Seed built-in roles before assigning membership — member
|
|
146
|
-
// rows reference roles by name, so they must exist first.
|
|
147
|
-
await db.seedBuiltinRoles(defaultOrg.id);
|
|
148
|
-
|
|
149
|
-
// Add super admin as owner
|
|
150
|
-
await db.addMember({
|
|
151
|
-
organizationId: defaultOrg.id,
|
|
152
|
-
userId: session.user.id,
|
|
153
|
-
role: 'owner'
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
// Set as active organization
|
|
157
|
-
await db.updateUserSession(session.user.id, defaultOrg.id);
|
|
158
|
-
|
|
159
|
-
cmsLogger.info(
|
|
160
|
-
'[AuthService]',
|
|
161
|
-
`Created default organization ${defaultOrg.id} for super admin.`
|
|
162
|
-
);
|
|
163
|
-
return {
|
|
164
|
-
type: 'session',
|
|
165
|
-
user: cmsUser,
|
|
166
|
-
session: {
|
|
167
|
-
id: session.session.id,
|
|
168
|
-
expiresAt: session.session.expiresAt
|
|
169
|
-
},
|
|
170
|
-
organizationId: defaultOrg.id,
|
|
171
|
-
organizationRole: 'owner'
|
|
172
|
-
};
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
// User has no organizations — return partial session without org context
|
|
176
|
-
// Routes like /invitations can use this to identify the user
|
|
177
|
-
cmsLogger.debug(
|
|
178
|
-
'[AuthService]',
|
|
179
|
-
`User ${session.user.id} has no organizations. Returning partial session.`
|
|
180
|
-
);
|
|
181
|
-
return {
|
|
182
|
-
type: 'partial_session',
|
|
183
|
-
user: cmsUser,
|
|
184
|
-
session: {
|
|
185
|
-
id: session.session.id,
|
|
186
|
-
expiresAt: session.session.expiresAt
|
|
187
|
-
}
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// Set the first organization as active
|
|
192
|
-
const firstOrg = userOrgs[0]!;
|
|
193
|
-
await db.updateUserSession(session.user.id, firstOrg.organization.id);
|
|
194
|
-
|
|
195
|
-
cmsLogger.debug(
|
|
196
|
-
'[AuthService]',
|
|
197
|
-
`Set first organization ${firstOrg.organization.id} as active.`
|
|
198
|
-
);
|
|
199
|
-
return {
|
|
200
|
-
type: 'session',
|
|
201
|
-
user: cmsUser,
|
|
202
|
-
session: {
|
|
203
|
-
id: session.session.id,
|
|
204
|
-
expiresAt: session.session.expiresAt
|
|
205
|
-
},
|
|
206
|
-
organizationId: firstOrg.organization.id,
|
|
207
|
-
organizationRole: firstOrg.member.role
|
|
208
|
-
};
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
// 6. Get the user's membership in the active organization
|
|
212
|
-
cmsLogger.debug(
|
|
213
|
-
'[AuthService]',
|
|
214
|
-
`Getting membership for org ${userSession.activeOrganizationId}`
|
|
215
|
-
);
|
|
216
|
-
const membership = await db.findUserMembership(
|
|
217
|
-
session.user.id,
|
|
218
|
-
userSession.activeOrganizationId!
|
|
219
|
-
);
|
|
220
|
-
|
|
221
|
-
if (!membership) {
|
|
222
|
-
cmsLogger.error(
|
|
223
|
-
'[AuthService]',
|
|
224
|
-
`User ${session.user.id} is not a member of org ${userSession.activeOrganizationId}`
|
|
225
|
-
);
|
|
226
|
-
throw new AuthError('kicked_from_org', 'User is not a member of the active organization');
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
// 7. Return the complete SessionAuth object with organization context
|
|
230
|
-
return {
|
|
231
|
-
type: 'session',
|
|
232
|
-
user: cmsUser,
|
|
233
|
-
session: {
|
|
234
|
-
id: session.session.id,
|
|
235
|
-
expiresAt: session.session.expiresAt
|
|
236
|
-
},
|
|
237
|
-
organizationId: userSession.activeOrganizationId!,
|
|
238
|
-
organizationRole: membership.role
|
|
239
|
-
};
|
|
240
|
-
} catch (error) {
|
|
241
|
-
// Re-throw AuthError to preserve error codes
|
|
242
|
-
if (error instanceof AuthError) {
|
|
243
|
-
throw error;
|
|
244
|
-
}
|
|
245
|
-
cmsLogger.error('[AuthService]', 'Error in getSession:', error);
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
},
|
|
249
|
-
|
|
250
|
-
async requireSession(request: Request, db: DatabaseAdapter): Promise<SessionAuth> {
|
|
251
|
-
const session = await this.getSession(request, db);
|
|
252
|
-
if (!session) {
|
|
253
|
-
throw new AuthError('no_session', 'Unauthorized: Session required');
|
|
254
|
-
}
|
|
255
|
-
// User is authenticated but has no organization — redirect to invitations
|
|
256
|
-
if (session.type === 'partial_session') {
|
|
257
|
-
throw new AuthError('pending_invitations', 'User has pending invitations to review');
|
|
258
|
-
}
|
|
259
|
-
return session;
|
|
260
|
-
},
|
|
261
|
-
|
|
262
|
-
async validateApiKey(request: Request): Promise<ApiKeyAuth | null> {
|
|
263
|
-
try {
|
|
264
|
-
const apiKeyHeader = request.headers.get('x-api-key');
|
|
265
|
-
if (!apiKeyHeader) return null;
|
|
266
|
-
|
|
267
|
-
const result = await auth.api.verifyApiKey({ body: { key: apiKeyHeader } });
|
|
268
|
-
if (!result.valid || !result.key) return null;
|
|
269
|
-
|
|
270
|
-
// Better Auth stores metadata in the key object
|
|
271
|
-
const metadata = result.key.metadata || {};
|
|
272
|
-
const permissions = metadata.permissions || ['read', 'write'];
|
|
273
|
-
// Fine-grained capability allowlist — optional. When present, takes
|
|
274
|
-
// precedence over `permissions` at the capability-resolution layer
|
|
275
|
-
// (see resolveCapabilities in cms-core).
|
|
276
|
-
const capabilities = Array.isArray(metadata.capabilities) ? metadata.capabilities : undefined;
|
|
277
|
-
const organizationId = metadata.organizationId;
|
|
278
|
-
|
|
279
|
-
if (!organizationId) {
|
|
280
|
-
cmsLogger.error(
|
|
281
|
-
'[AuthService]',
|
|
282
|
-
`API key ${result.key.id} missing organizationId in metadata`
|
|
283
|
-
);
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
return {
|
|
288
|
-
type: 'api_key',
|
|
289
|
-
keyId: result.key.id,
|
|
290
|
-
name: result.key.name || 'Unnamed Key',
|
|
291
|
-
permissions,
|
|
292
|
-
capabilities,
|
|
293
|
-
organizationId,
|
|
294
|
-
lastUsedAt: result.key.lastRequest || undefined
|
|
295
|
-
};
|
|
296
|
-
} catch (error) {
|
|
297
|
-
cmsLogger.error('[AuthService]', 'validateApiKey error:', error);
|
|
298
|
-
return null;
|
|
299
|
-
}
|
|
300
|
-
},
|
|
301
|
-
|
|
302
|
-
async requireApiKey(
|
|
303
|
-
request: Request,
|
|
304
|
-
db: DatabaseAdapter,
|
|
305
|
-
permission?: 'read' | 'write'
|
|
306
|
-
): Promise<ApiKeyAuth> {
|
|
307
|
-
const apiKeyAuth = await this.validateApiKey(request);
|
|
308
|
-
if (!apiKeyAuth) {
|
|
309
|
-
throw new Error('Unauthorized: Valid API key required');
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
if (permission && !apiKeyAuth.permissions.includes(permission)) {
|
|
313
|
-
throw new Error(`Forbidden: API key missing ${permission} permission`);
|
|
314
|
-
}
|
|
315
|
-
|
|
316
|
-
return apiKeyAuth;
|
|
317
|
-
},
|
|
318
|
-
|
|
319
|
-
async listApiKeys(db: DatabaseAdapter, userId: string): Promise<ApiKey[]> {
|
|
320
|
-
// Query the apikey table directly using drizzleDb (not the adapter)
|
|
321
|
-
const userApiKeys = await drizzleDb.query.apikey.findMany({
|
|
322
|
-
where: eq(apikey.referenceId, userId),
|
|
323
|
-
columns: {
|
|
324
|
-
id: true,
|
|
325
|
-
name: true,
|
|
326
|
-
metadata: true,
|
|
327
|
-
expiresAt: true,
|
|
328
|
-
lastRequest: true,
|
|
329
|
-
createdAt: true
|
|
330
|
-
},
|
|
331
|
-
orderBy: (apikey, { desc }) => [desc(apikey.createdAt)]
|
|
332
|
-
});
|
|
333
|
-
|
|
334
|
-
return userApiKeys.map((key) => {
|
|
335
|
-
const metadata =
|
|
336
|
-
typeof key.metadata === 'string' ? JSON.parse(key.metadata) : (key.metadata as any) || {};
|
|
337
|
-
return {
|
|
338
|
-
...key,
|
|
339
|
-
permissions: metadata.permissions || [],
|
|
340
|
-
capabilities: Array.isArray(metadata.capabilities) ? metadata.capabilities : undefined,
|
|
341
|
-
organizationId: metadata.organizationId
|
|
342
|
-
};
|
|
343
|
-
});
|
|
344
|
-
},
|
|
345
|
-
|
|
346
|
-
async createApiKey(
|
|
347
|
-
userId: string,
|
|
348
|
-
organizationId: string,
|
|
349
|
-
data: CreateApiKeyData
|
|
350
|
-
): Promise<ApiKeyWithSecret> {
|
|
351
|
-
const expiresIn = data.expiresInDays ? data.expiresInDays * 24 * 60 * 60 : undefined;
|
|
352
|
-
|
|
353
|
-
const result = await auth.api.createApiKey({
|
|
354
|
-
body: {
|
|
355
|
-
userId: userId,
|
|
356
|
-
name: data.name,
|
|
357
|
-
expiresIn,
|
|
358
|
-
metadata: {
|
|
359
|
-
// Keep both fields for forward/backward compatibility:
|
|
360
|
-
// older validators read `permissions`, newer ones prefer
|
|
361
|
-
// `capabilities` when the caller supplied them.
|
|
362
|
-
permissions: data.permissions ?? ['read'],
|
|
363
|
-
capabilities: data.capabilities,
|
|
364
|
-
organizationId: organizationId
|
|
365
|
-
}
|
|
366
|
-
}
|
|
367
|
-
});
|
|
368
|
-
|
|
369
|
-
if (!result || !result.id) {
|
|
370
|
-
throw new Error('Failed to create API key');
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
return {
|
|
374
|
-
id: result.id,
|
|
375
|
-
name: result.name,
|
|
376
|
-
key: result.key,
|
|
377
|
-
permissions: data.permissions ?? ['read'],
|
|
378
|
-
capabilities: data.capabilities,
|
|
379
|
-
organizationId: organizationId,
|
|
380
|
-
expiresAt: result.expiresAt,
|
|
381
|
-
createdAt: result.createdAt
|
|
382
|
-
};
|
|
383
|
-
},
|
|
384
|
-
|
|
385
|
-
async deleteApiKey(userId: string, keyId: string): Promise<boolean> {
|
|
386
|
-
// This will be implemented when we refactor the [id] route
|
|
387
|
-
cmsLogger.debug('[AuthService]', `Deleting key ${keyId} for user ${userId}`);
|
|
388
|
-
return Promise.resolve(true);
|
|
389
|
-
},
|
|
390
|
-
|
|
391
|
-
async getUserById(userId: string): Promise<{ id: string; name?: string; email: string } | null> {
|
|
392
|
-
try {
|
|
393
|
-
const userRecord = await drizzleDb.query.user.findFirst({
|
|
394
|
-
where: eq(user.id, userId),
|
|
395
|
-
columns: {
|
|
396
|
-
id: true,
|
|
397
|
-
name: true,
|
|
398
|
-
email: true
|
|
399
|
-
}
|
|
400
|
-
});
|
|
401
|
-
|
|
402
|
-
if (!userRecord) {
|
|
403
|
-
return null;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
return {
|
|
407
|
-
id: userRecord.id,
|
|
408
|
-
name: userRecord.name ?? undefined,
|
|
409
|
-
email: userRecord.email
|
|
410
|
-
};
|
|
411
|
-
} catch (error) {
|
|
412
|
-
cmsLogger.error('[AuthService]', 'Error fetching user by ID:', error);
|
|
413
|
-
return null;
|
|
414
|
-
}
|
|
415
|
-
},
|
|
416
|
-
|
|
417
|
-
async getUserByEmail(
|
|
418
|
-
email: string
|
|
419
|
-
): Promise<{ id: string; name?: string; email: string } | null> {
|
|
420
|
-
try {
|
|
421
|
-
const userRecord = await drizzleDb.query.user.findFirst({
|
|
422
|
-
where: eq(user.email, email.toLowerCase()),
|
|
423
|
-
columns: {
|
|
424
|
-
id: true,
|
|
425
|
-
name: true,
|
|
426
|
-
email: true
|
|
427
|
-
}
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
if (!userRecord) {
|
|
431
|
-
return null;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
return {
|
|
435
|
-
id: userRecord.id,
|
|
436
|
-
name: userRecord.name ?? undefined,
|
|
437
|
-
email: userRecord.email
|
|
438
|
-
};
|
|
439
|
-
} catch (error) {
|
|
440
|
-
cmsLogger.error('[AuthService]', 'Error fetching user by email:', error);
|
|
441
|
-
return null;
|
|
442
|
-
}
|
|
443
|
-
},
|
|
444
|
-
|
|
445
|
-
async changeUserName(userId: string, name: string): Promise<void> {
|
|
446
|
-
await drizzleDb
|
|
447
|
-
.update(user)
|
|
448
|
-
.set({
|
|
449
|
-
name,
|
|
450
|
-
updatedAt: new Date()
|
|
451
|
-
})
|
|
452
|
-
.where(eq(user.id, userId));
|
|
453
|
-
},
|
|
454
|
-
|
|
455
|
-
async requestPasswordReset(email: string, redirectTo?: string): Promise<void> {
|
|
456
|
-
try {
|
|
457
|
-
await auth.api.requestPasswordReset({
|
|
458
|
-
body: {
|
|
459
|
-
email,
|
|
460
|
-
redirectTo
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
|
|
464
|
-
// TODO: Send password reset email via email adapter - for true agnoticism
|
|
465
|
-
// The email adapter can be accessed from event.locals.aphexCMS.emailAdapter
|
|
466
|
-
// For now, Better Auth handles the email sending internally
|
|
467
|
-
} catch (error) {
|
|
468
|
-
cmsLogger.error('[AuthService]', 'Error requesting password reset:', error);
|
|
469
|
-
throw error;
|
|
470
|
-
}
|
|
471
|
-
},
|
|
472
|
-
|
|
473
|
-
async resetPassword(token: string, newPassword: string): Promise<void> {
|
|
474
|
-
try {
|
|
475
|
-
await auth.api.resetPassword({
|
|
476
|
-
body: {
|
|
477
|
-
newPassword,
|
|
478
|
-
token
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
} catch (error) {
|
|
482
|
-
cmsLogger.error('[AuthService]', 'Error resetting password:', error);
|
|
483
|
-
throw error;
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
};
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { InMemoryCacheAdapter } from '@aphexcms/cms-core/server';
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Shared cache adapter singleton.
|
|
5
|
-
* Used by both CMS config (document caching) and auth (API key caching).
|
|
6
|
-
* Set to null to disable caching.
|
|
7
|
-
*/
|
|
8
|
-
export const cacheAdapter: InMemoryCacheAdapter | null = new InMemoryCacheAdapter({
|
|
9
|
-
maxSize: 5000
|
|
10
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
import { pgTable, text, timestamp, boolean, integer } from 'drizzle-orm/pg-core';
|
|
2
|
-
|
|
3
|
-
export const user = pgTable('user', {
|
|
4
|
-
id: text('id').primaryKey(),
|
|
5
|
-
name: text('name').notNull(),
|
|
6
|
-
email: text('email').notNull().unique(),
|
|
7
|
-
emailVerified: boolean('email_verified').default(false).notNull(),
|
|
8
|
-
image: text('image'),
|
|
9
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
10
|
-
updatedAt: timestamp('updated_at')
|
|
11
|
-
.defaultNow()
|
|
12
|
-
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
13
|
-
.notNull()
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
export const session = pgTable('session', {
|
|
17
|
-
id: text('id').primaryKey(),
|
|
18
|
-
expiresAt: timestamp('expires_at').notNull(),
|
|
19
|
-
token: text('token').notNull().unique(),
|
|
20
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
21
|
-
updatedAt: timestamp('updated_at')
|
|
22
|
-
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
23
|
-
.notNull(),
|
|
24
|
-
ipAddress: text('ip_address'),
|
|
25
|
-
userAgent: text('user_agent'),
|
|
26
|
-
userId: text('user_id')
|
|
27
|
-
.notNull()
|
|
28
|
-
.references(() => user.id, { onDelete: 'cascade' })
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
export const account = pgTable('account', {
|
|
32
|
-
id: text('id').primaryKey(),
|
|
33
|
-
accountId: text('account_id').notNull(),
|
|
34
|
-
providerId: text('provider_id').notNull(),
|
|
35
|
-
userId: text('user_id')
|
|
36
|
-
.notNull()
|
|
37
|
-
.references(() => user.id, { onDelete: 'cascade' }),
|
|
38
|
-
accessToken: text('access_token'),
|
|
39
|
-
refreshToken: text('refresh_token'),
|
|
40
|
-
idToken: text('id_token'),
|
|
41
|
-
accessTokenExpiresAt: timestamp('access_token_expires_at'),
|
|
42
|
-
refreshTokenExpiresAt: timestamp('refresh_token_expires_at'),
|
|
43
|
-
scope: text('scope'),
|
|
44
|
-
password: text('password'),
|
|
45
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
46
|
-
updatedAt: timestamp('updated_at')
|
|
47
|
-
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
48
|
-
.notNull()
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
export const verification = pgTable('verification', {
|
|
52
|
-
id: text('id').primaryKey(),
|
|
53
|
-
identifier: text('identifier').notNull(),
|
|
54
|
-
value: text('value').notNull(),
|
|
55
|
-
expiresAt: timestamp('expires_at').notNull(),
|
|
56
|
-
createdAt: timestamp('created_at').defaultNow().notNull(),
|
|
57
|
-
updatedAt: timestamp('updated_at')
|
|
58
|
-
.defaultNow()
|
|
59
|
-
.$onUpdate(() => /* @__PURE__ */ new Date())
|
|
60
|
-
.notNull()
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
export const apikey = pgTable('apikey', {
|
|
64
|
-
id: text('id').primaryKey(),
|
|
65
|
-
configId: text('config_id').notNull().default('default'),
|
|
66
|
-
name: text('name'),
|
|
67
|
-
start: text('start'),
|
|
68
|
-
prefix: text('prefix'),
|
|
69
|
-
key: text('key').notNull(),
|
|
70
|
-
referenceId: text('reference_id').notNull(),
|
|
71
|
-
refillInterval: integer('refill_interval'),
|
|
72
|
-
refillAmount: integer('refill_amount'),
|
|
73
|
-
lastRefillAt: timestamp('last_refill_at'),
|
|
74
|
-
enabled: boolean('enabled').default(true),
|
|
75
|
-
rateLimitEnabled: boolean('rate_limit_enabled').default(true),
|
|
76
|
-
rateLimitTimeWindow: integer('rate_limit_time_window').default(86400000),
|
|
77
|
-
rateLimitMax: integer('rate_limit_max').default(10000),
|
|
78
|
-
requestCount: integer('request_count').default(0),
|
|
79
|
-
remaining: integer('remaining'),
|
|
80
|
-
lastRequest: timestamp('last_request'),
|
|
81
|
-
expiresAt: timestamp('expires_at'),
|
|
82
|
-
createdAt: timestamp('created_at').notNull(),
|
|
83
|
-
updatedAt: timestamp('updated_at').notNull(),
|
|
84
|
-
permissions: text('permissions'),
|
|
85
|
-
metadata: text('metadata')
|
|
86
|
-
});
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
// Database schema for Aphex CMS using Drizzle ORM
|
|
2
|
-
// This file combines CMS package schema with app-specific tables
|
|
3
|
-
// Re-export CMS core schema tables from PostgreSQL adapter package
|
|
4
|
-
// Import from /schema to avoid loading the entire adapter
|
|
5
|
-
export {
|
|
6
|
-
// Multi-tenancy tables
|
|
7
|
-
organizations,
|
|
8
|
-
organizationMembers,
|
|
9
|
-
invitations,
|
|
10
|
-
roles,
|
|
11
|
-
instanceSettings,
|
|
12
|
-
userSessions,
|
|
13
|
-
// Content tables
|
|
14
|
-
documents,
|
|
15
|
-
documentVersions,
|
|
16
|
-
assets,
|
|
17
|
-
schemaTypes,
|
|
18
|
-
userProfiles,
|
|
19
|
-
// Enums
|
|
20
|
-
documentStatusEnum,
|
|
21
|
-
versionEventEnum,
|
|
22
|
-
schemaTypeEnum
|
|
23
|
-
} from '@aphexcms/postgresql-adapter/schema';
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { drizzle } from 'drizzle-orm/postgres-js';
|
|
2
|
-
import type { Logger } from 'drizzle-orm';
|
|
3
|
-
import postgres from 'postgres';
|
|
4
|
-
import { env } from '$env/dynamic/private';
|
|
5
|
-
import { createPostgreSQLProvider, pgConnectionUrl } from '@aphexcms/postgresql-adapter';
|
|
6
|
-
import * as cmsSchema from './cms-schema';
|
|
7
|
-
import * as authSchema from './auth-schema';
|
|
8
|
-
|
|
9
|
-
const schema = {
|
|
10
|
-
...cmsSchema,
|
|
11
|
-
...authSchema
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
import type { DatabaseAdapter } from '@aphexcms/cms-core/server';
|
|
15
|
-
|
|
16
|
-
const SLOW_QUERY_THRESHOLD_MS = parseInt(env.SLOW_QUERY_MS || '100');
|
|
17
|
-
|
|
18
|
-
class SlowQueryLogger implements Logger {
|
|
19
|
-
logQuery(query: string, params: unknown[]): void {
|
|
20
|
-
const start = performance.now();
|
|
21
|
-
// Store start time — Drizzle calls logQuery before execution
|
|
22
|
-
// We use queueMicrotask to measure after the query completes
|
|
23
|
-
queueMicrotask(() => {
|
|
24
|
-
const duration = performance.now() - start;
|
|
25
|
-
if (duration >= SLOW_QUERY_THRESHOLD_MS) {
|
|
26
|
-
const truncatedQuery = query.length > 200 ? query.slice(0, 200) + '...' : query;
|
|
27
|
-
console.warn(`[SLOW QUERY] ${duration.toFixed(1)}ms — ${truncatedQuery}`);
|
|
28
|
-
}
|
|
29
|
-
});
|
|
30
|
-
}
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
const databaseUrl = pgConnectionUrl(env);
|
|
34
|
-
|
|
35
|
-
export const client = postgres(databaseUrl, {
|
|
36
|
-
max: 50,
|
|
37
|
-
idle_timeout: 20, // Release idle connections after 20s
|
|
38
|
-
connect_timeout: 10, // Fail fast if can't connect in 10s
|
|
39
|
-
max_lifetime: 60 * 5 // Recycle connections every 5 minutes
|
|
40
|
-
});
|
|
41
|
-
export const drizzleDb = drizzle(client, {
|
|
42
|
-
schema,
|
|
43
|
-
logger: env.ENABLE_QUERY_LOG === 'true' ? new SlowQueryLogger() : undefined
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
const provider = createPostgreSQLProvider({
|
|
47
|
-
client,
|
|
48
|
-
multiTenancy: {
|
|
49
|
-
enableRLS: true,
|
|
50
|
-
enableHierarchy: true
|
|
51
|
-
}
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
const adapter = provider.createAdapter();
|
|
55
|
-
export const db = adapter as DatabaseAdapter;
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
import { Html, Head, Body, Preview, Container, Section, Text, Hr } from 'better-svelte-email';
|
|
3
|
-
import type { Snippet } from 'svelte';
|
|
4
|
-
|
|
5
|
-
let { preview = '', children }: { preview?: string; children: Snippet } = $props();
|
|
6
|
-
</script>
|
|
7
|
-
|
|
8
|
-
<Html>
|
|
9
|
-
<Head />
|
|
10
|
-
<Body class="bg-[#f6f9fc] font-sans">
|
|
11
|
-
{#if preview}
|
|
12
|
-
<Preview {preview} />
|
|
13
|
-
{/if}
|
|
14
|
-
<Container class="mx-auto my-[40px] max-w-[580px] rounded-[8px] bg-white p-[32px]">
|
|
15
|
-
{@render children()}
|
|
16
|
-
|
|
17
|
-
<Hr class="my-[24px] border-[#e6ebf1]" />
|
|
18
|
-
|
|
19
|
-
<Section>
|
|
20
|
-
<Text class="text-[12px] leading-[16px] text-[#8898aa]">Sent by Aphex CMS</Text>
|
|
21
|
-
</Section>
|
|
22
|
-
</Container>
|
|
23
|
-
</Body>
|
|
24
|
-
</Html>
|