anhedral 0.1.0 → 0.1.3
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/README.md +82 -20
- package/dist/branding.d.ts +2 -0
- package/dist/branding.js +9 -1
- package/dist/cli.d.ts +12 -0
- package/dist/cli.js +86 -0
- package/dist/commands.d.ts +2 -3
- package/dist/commands.js +9 -33
- package/dist/index.js +8 -134
- package/dist/print.d.ts +8 -0
- package/dist/print.js +33 -0
- package/dist/scaffold.d.ts +3 -3
- package/dist/scaffold.js +770 -381
- package/dist/templates/backend.js +76 -190
- package/dist/templates/default-web.js +819 -237
- package/dist/templates/extension.js +200 -18
- package/dist/templates/frontend.js +12 -11
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +3 -6
- package/dist/util.d.ts +1 -1
- package/dist/util.js +100 -43
- package/favicon.ico +0 -0
- package/package.json +16 -10
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { writeFile, exec } from '../util.js';
|
|
3
|
+
import { anhedralPrint } from '../print.js';
|
|
3
4
|
import { getBackendInstallCommands } from '../commands.js';
|
|
4
5
|
export async function scaffoldBackend(root, { projectName, displayName, frontendUrl }) {
|
|
5
|
-
const dir = path.join(root, '
|
|
6
|
-
|
|
6
|
+
const dir = path.join(root, 'apps/api');
|
|
7
|
+
anhedralPrint.section('Backend (Fastify)');
|
|
8
|
+
anhedralPrint.step('Writing backend source files');
|
|
7
9
|
writePackageJson(dir, projectName);
|
|
8
10
|
writeTsConfig(dir);
|
|
9
11
|
writeEslintConfig(dir);
|
|
@@ -12,7 +14,6 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
|
|
|
12
14
|
writeEnvFile(dir, frontendUrl);
|
|
13
15
|
writeDrizzleConfig(dir);
|
|
14
16
|
writeVitestConfig(dir);
|
|
15
|
-
writeVercelJson(dir);
|
|
16
17
|
writeTypes(dir);
|
|
17
18
|
writeDbFiles(dir);
|
|
18
19
|
writeErrorFiles(dir);
|
|
@@ -24,9 +25,16 @@ export async function scaffoldBackend(root, { projectName, displayName, frontend
|
|
|
24
25
|
writeAppAndIndex(dir, displayName);
|
|
25
26
|
writeVercelEntry(dir);
|
|
26
27
|
writeTestFiles(dir);
|
|
28
|
+
anhedralPrint.done('Backend source files written');
|
|
29
|
+
if (process.env.ANHEDRAL_SKIP_INSTALL === '1') {
|
|
30
|
+
anhedralPrint.info('Skipping backend dependency install (ANHEDRAL_SKIP_INSTALL=1)');
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
anhedralPrint.step('Installing backend dependencies');
|
|
27
34
|
for (const command of getBackendInstallCommands()) {
|
|
28
35
|
exec(command.cmd, dir);
|
|
29
36
|
}
|
|
37
|
+
anhedralPrint.done('Backend dependencies installed');
|
|
30
38
|
}
|
|
31
39
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
32
40
|
// Config files
|
|
@@ -42,7 +50,7 @@ function writePackageJson(dir, projectName) {
|
|
|
42
50
|
build: 'pnpm typecheck',
|
|
43
51
|
typecheck: 'tsc --noEmit',
|
|
44
52
|
'db:generate': 'drizzle-kit generate',
|
|
45
|
-
'db:migrate': 'pnpm
|
|
53
|
+
'db:migrate': 'pnpm --filter @anhedral/db db:migrate',
|
|
46
54
|
'db:studio': 'drizzle-kit studio',
|
|
47
55
|
'db:push': 'drizzle-kit push',
|
|
48
56
|
lint: 'eslint . --ext .js,.ts',
|
|
@@ -51,14 +59,17 @@ function writePackageJson(dir, projectName) {
|
|
|
51
59
|
},
|
|
52
60
|
keywords: [],
|
|
53
61
|
license: 'MIT',
|
|
62
|
+
dependencies: {
|
|
63
|
+
'@anhedral/db': 'workspace:*',
|
|
64
|
+
},
|
|
54
65
|
}, null, 2) + '\n');
|
|
55
66
|
}
|
|
56
67
|
function writeTsConfig(dir) {
|
|
57
68
|
writeFile(path.join(dir, 'tsconfig.json'), JSON.stringify({
|
|
58
69
|
compilerOptions: {
|
|
59
70
|
target: 'ESNext',
|
|
60
|
-
module: '
|
|
61
|
-
moduleResolution: '
|
|
71
|
+
module: 'ESNext',
|
|
72
|
+
moduleResolution: 'Bundler',
|
|
62
73
|
skipLibCheck: true,
|
|
63
74
|
rootDir: '.',
|
|
64
75
|
outDir: 'dist',
|
|
@@ -104,6 +115,7 @@ function writeEnvExample(dir, frontendUrl = 'http://localhost:8081') {
|
|
|
104
115
|
PORT=8787
|
|
105
116
|
NODE_ENV=development
|
|
106
117
|
LOG_LEVEL=info
|
|
118
|
+
ANHEDRAL_DEMO=false
|
|
107
119
|
|
|
108
120
|
# Database (NeonDB)
|
|
109
121
|
DATABASE_URL="postgresql://neondb_owner:***@***.neon.tech/neondb?sslmode=require"
|
|
@@ -134,6 +146,7 @@ function writeEnvFile(dir, frontendUrl = 'http://localhost:8081') {
|
|
|
134
146
|
PORT=8787
|
|
135
147
|
NODE_ENV=development
|
|
136
148
|
LOG_LEVEL=info
|
|
149
|
+
ANHEDRAL_DEMO=true
|
|
137
150
|
|
|
138
151
|
# Database (NeonDB)
|
|
139
152
|
DATABASE_URL="postgresql://user:pass@localhost:5432/app?sslmode=disable"
|
|
@@ -162,8 +175,8 @@ RC_OFFERING_ID=default
|
|
|
162
175
|
function writeDrizzleConfig(dir) {
|
|
163
176
|
writeFile(path.join(dir, 'drizzle.config.ts'), `import type { Config } from 'drizzle-kit';
|
|
164
177
|
export default {
|
|
165
|
-
schema: '
|
|
166
|
-
out: '
|
|
178
|
+
schema: '../../packages/db/src/schema.ts',
|
|
179
|
+
out: '../../packages/db/migrations',
|
|
167
180
|
dialect: 'postgresql',
|
|
168
181
|
dbCredentials: { url: process.env.DATABASE_URL! }
|
|
169
182
|
} satisfies Config;
|
|
@@ -184,13 +197,6 @@ export default defineConfig({
|
|
|
184
197
|
});
|
|
185
198
|
`);
|
|
186
199
|
}
|
|
187
|
-
function writeVercelJson(dir) {
|
|
188
|
-
writeFile(path.join(dir, 'vercel.json'), JSON.stringify({
|
|
189
|
-
version: 2,
|
|
190
|
-
builds: [{ src: 'api/index.ts', use: '@vercel/node' }],
|
|
191
|
-
routes: [{ src: '/(.*)', dest: 'api/index.ts' }],
|
|
192
|
-
}, null, 2) + '\n');
|
|
193
|
-
}
|
|
194
200
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
195
201
|
// Types
|
|
196
202
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -207,6 +213,7 @@ export interface AppEnv {
|
|
|
207
213
|
PORT: number;
|
|
208
214
|
NODE_ENV: string;
|
|
209
215
|
LOG_LEVEL: string;
|
|
216
|
+
ANHEDRAL_DEMO?: string | null;
|
|
210
217
|
CLERK_PUBLISHABLE_KEY?: string | null;
|
|
211
218
|
CLERK_SECRET_KEY?: string | null;
|
|
212
219
|
FRONTEND_URL?: string | null;
|
|
@@ -265,183 +272,12 @@ export {};
|
|
|
265
272
|
// Database
|
|
266
273
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
267
274
|
function writeDbFiles(dir) {
|
|
268
|
-
writeFile(path.join(dir, 'src/db/index.ts'), `
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
if (!process.env.DATABASE_URL) {
|
|
272
|
-
throw new Error('DATABASE_URL is not defined');
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
const sql = neon(process.env.DATABASE_URL);
|
|
276
|
-
export const db = drizzle(sql);
|
|
277
|
-
export type Database = typeof db;
|
|
275
|
+
writeFile(path.join(dir, 'src/db/index.ts'), `export { db } from '@anhedral/db';
|
|
276
|
+
export type { Database } from '@anhedral/db';
|
|
278
277
|
`);
|
|
279
|
-
writeFile(path.join(dir, 'src/db/schema.ts'), `
|
|
280
|
-
import type { InferSelectModel, InferInsertModel } from 'drizzle-orm';
|
|
281
|
-
|
|
282
|
-
export const SUBSCRIPTION_TIERS = ['free', 'pro'] as const;
|
|
283
|
-
export type SubscriptionTier = (typeof SUBSCRIPTION_TIERS)[number];
|
|
284
|
-
|
|
285
|
-
export const SUBSCRIPTION_STATUSES = ['active', 'expired', 'canceled'] as const;
|
|
286
|
-
export type SubscriptionStatus = (typeof SUBSCRIPTION_STATUSES)[number];
|
|
287
|
-
|
|
288
|
-
export const SUBSCRIPTION_METHODS = ['trialing', 'redeemed', 'paid'] as const;
|
|
289
|
-
export type SubscriptionMethod = (typeof SUBSCRIPTION_METHODS)[number];
|
|
290
|
-
|
|
291
|
-
export const SUBSCRIPTION_ORIGINS = ['web', 'apple', 'google'] as const;
|
|
292
|
-
export type SubscriptionOrigin = (typeof SUBSCRIPTION_ORIGINS)[number];
|
|
293
|
-
|
|
294
|
-
export const SUBSCRIPTION_EVENT_TYPES = [
|
|
295
|
-
'trial_started', 'trial_converted', 'trial_expired',
|
|
296
|
-
'initial_purchase', 'renewal', 'product_change',
|
|
297
|
-
'cancellation_scheduled', 'cancellation_unscheduled', 'subscription_expired', 'subscription_canceled',
|
|
298
|
-
'promo_redeemed', 'billing_issue', 'billing_recovered',
|
|
299
|
-
] as const;
|
|
300
|
-
export type SubscriptionEventType = (typeof SUBSCRIPTION_EVENT_TYPES)[number];
|
|
301
|
-
|
|
302
|
-
export type SubscriptionMetadata = {
|
|
303
|
-
revenueCatProductId?: string; lastWebhookUpdate?: string; cancelReason?: string;
|
|
304
|
-
redeemCode?: string; redeemCodeRedeemedAt?: string;
|
|
305
|
-
};
|
|
306
|
-
|
|
307
|
-
export type SubscriptionEventMetadata = {
|
|
308
|
-
revenueCatEventType?: string; revenueCatProductId?: string; promoCode?: string;
|
|
309
|
-
billingPeriod?: string; price?: { amount: number; currency: string };
|
|
310
|
-
store?: string; transactionId?: string; reason?: string;
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
export const PROMO_CODE_DURATIONS = [1, 6, 12] as const;
|
|
314
|
-
export type PromoCodeDuration = (typeof PROMO_CODE_DURATIONS)[number];
|
|
315
|
-
|
|
316
|
-
export const users = pgTable('users', {
|
|
317
|
-
id: text('id').primaryKey(),
|
|
318
|
-
email: text('email').notNull().unique(),
|
|
319
|
-
displayName: text('display_name'),
|
|
320
|
-
lastLoginAt: timestamp('last_login_at'),
|
|
321
|
-
profileImageUrl: text('profile_image_url'),
|
|
322
|
-
avatarObjectKey: text('avatar_object_key'),
|
|
323
|
-
avatarMimeType: text('avatar_mime_type'),
|
|
324
|
-
creditsBalance: integer('credits_balance').notNull().default(250),
|
|
325
|
-
createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
export const subscriptions = pgTable('subscriptions', {
|
|
329
|
-
id: text('id').primaryKey(),
|
|
330
|
-
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }).unique(),
|
|
331
|
-
tier: text('tier').$type<SubscriptionTier>().notNull().default('free'),
|
|
332
|
-
status: text('status').$type<SubscriptionStatus>().notNull().default('active'),
|
|
333
|
-
method: text('method').$type<SubscriptionMethod>(),
|
|
334
|
-
origin: text('origin').$type<SubscriptionOrigin>(),
|
|
335
|
-
billingPeriod: text('billing_period'),
|
|
336
|
-
currentPeriodStart: timestamp('current_period_start'),
|
|
337
|
-
currentPeriodEnd: timestamp('current_period_end'),
|
|
338
|
-
canceledAt: timestamp('canceled_at'),
|
|
339
|
-
cancelAtPeriodEnd: boolean('cancel_at_period_end').notNull().default(false),
|
|
340
|
-
trialStart: timestamp('trial_start'),
|
|
341
|
-
trialEnd: timestamp('trial_end'),
|
|
342
|
-
dailyLimit: integer('daily_limit'),
|
|
343
|
-
metadata: jsonb('metadata').$type<SubscriptionMetadata>(),
|
|
344
|
-
createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
|
|
345
|
-
updatedAt: timestamp('updated_at').$defaultFn(() => new Date()).notNull(),
|
|
346
|
-
}, (t) => [
|
|
347
|
-
index('subscriptions_user_idx').on(t.userId),
|
|
348
|
-
index('subscriptions_status_idx').on(t.status),
|
|
349
|
-
index('subscriptions_tier_idx').on(t.tier),
|
|
350
|
-
index('subscriptions_period_end_idx').on(t.currentPeriodEnd),
|
|
351
|
-
]);
|
|
352
|
-
|
|
353
|
-
export const subscriptionEvents = pgTable('subscription_events', {
|
|
354
|
-
id: text('id').primaryKey(),
|
|
355
|
-
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
356
|
-
subscriptionId: text('subscription_id').references(() => subscriptions.id, { onDelete: 'set null' }),
|
|
357
|
-
eventType: text('event_type').$type<SubscriptionEventType>().notNull(),
|
|
358
|
-
previousTier: text('previous_tier').$type<SubscriptionTier>(),
|
|
359
|
-
previousStatus: text('previous_status').$type<SubscriptionStatus>(),
|
|
360
|
-
previousMethod: text('previous_method').$type<SubscriptionMethod>(),
|
|
361
|
-
newTier: text('new_tier').$type<SubscriptionTier>(),
|
|
362
|
-
newStatus: text('new_status').$type<SubscriptionStatus>(),
|
|
363
|
-
newMethod: text('new_method').$type<SubscriptionMethod>(),
|
|
364
|
-
revenueCatEventType: text('revenuecat_event_type'),
|
|
365
|
-
revenueCatProductId: text('revenuecat_product_id'),
|
|
366
|
-
origin: text('origin').$type<SubscriptionOrigin>(),
|
|
367
|
-
periodStart: timestamp('period_start'),
|
|
368
|
-
periodEnd: timestamp('period_end'),
|
|
369
|
-
metadata: jsonb('metadata').$type<SubscriptionEventMetadata>(),
|
|
370
|
-
createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
|
|
371
|
-
}, (t) => [
|
|
372
|
-
index('sub_events_user_idx').on(t.userId),
|
|
373
|
-
index('sub_events_user_created_idx').on(t.userId, t.createdAt),
|
|
374
|
-
index('sub_events_type_idx').on(t.eventType),
|
|
375
|
-
]);
|
|
376
|
-
|
|
377
|
-
export const trialClaims = pgTable('trial_claims', {
|
|
378
|
-
id: text('id').primaryKey(),
|
|
379
|
-
userId: text('user_id').references(() => users.id, { onDelete: 'set null' }),
|
|
380
|
-
email: text('email').notNull().unique(),
|
|
381
|
-
claimedAt: timestamp('claimed_at').$defaultFn(() => new Date()).notNull(),
|
|
382
|
-
}, (t) => [index('trial_claims_email_idx').on(t.email)]);
|
|
383
|
-
|
|
384
|
-
export const promoCodes = pgTable('promo_codes', {
|
|
385
|
-
id: text('id').primaryKey(),
|
|
386
|
-
code: text('code').notNull().unique(),
|
|
387
|
-
months: integer('months').$type<PromoCodeDuration>().notNull(),
|
|
388
|
-
maxRedemptions: integer('max_redemptions').notNull().default(1),
|
|
389
|
-
redeemedCount: integer('redeemed_count').notNull().default(0),
|
|
390
|
-
expiresAt: timestamp('expires_at'),
|
|
391
|
-
createdAt: timestamp('created_at').$defaultFn(() => new Date()).notNull(),
|
|
392
|
-
}, (t) => [index('promo_codes_code_idx').on(t.code)]);
|
|
393
|
-
|
|
394
|
-
export const promoRedemptions = pgTable('promo_redemptions', {
|
|
395
|
-
id: text('id').primaryKey(),
|
|
396
|
-
promoCodeId: text('promo_code_id').notNull().references(() => promoCodes.id, { onDelete: 'cascade' }),
|
|
397
|
-
userId: text('user_id').notNull().references(() => users.id, { onDelete: 'cascade' }),
|
|
398
|
-
redeemedAt: timestamp('redeemed_at').$defaultFn(() => new Date()).notNull(),
|
|
399
|
-
entitlementExpiresAt: timestamp('entitlement_expires_at').notNull(),
|
|
400
|
-
}, (t) => [
|
|
401
|
-
index('promo_redemptions_user_idx').on(t.userId),
|
|
402
|
-
index('promo_redemptions_code_idx').on(t.promoCodeId),
|
|
403
|
-
]);
|
|
404
|
-
|
|
405
|
-
export type Users = InferSelectModel<typeof users>;
|
|
406
|
-
export type NewUsers = InferInsertModel<typeof users>;
|
|
407
|
-
export type Subscriptions = InferSelectModel<typeof subscriptions>;
|
|
408
|
-
export type NewSubscriptions = InferInsertModel<typeof subscriptions>;
|
|
409
|
-
export type SubscriptionEvents = InferSelectModel<typeof subscriptionEvents>;
|
|
410
|
-
export type NewSubscriptionEvents = InferInsertModel<typeof subscriptionEvents>;
|
|
411
|
-
export type TrialClaims = InferSelectModel<typeof trialClaims>;
|
|
412
|
-
export type NewTrialClaims = InferInsertModel<typeof trialClaims>;
|
|
413
|
-
export type PromoCodes = InferSelectModel<typeof promoCodes>;
|
|
414
|
-
export type NewPromoCodes = InferInsertModel<typeof promoCodes>;
|
|
415
|
-
export type PromoRedemptions = InferSelectModel<typeof promoRedemptions>;
|
|
416
|
-
export type NewPromoRedemptions = InferInsertModel<typeof promoRedemptions>;
|
|
278
|
+
writeFile(path.join(dir, 'src/db/schema.ts'), `export * from '@anhedral/db/schema';
|
|
417
279
|
`);
|
|
418
|
-
writeFile(path.join(dir, 'src/db/migrate.ts'), `import
|
|
419
|
-
import { migrate } from 'drizzle-orm/neon-http/migrator';
|
|
420
|
-
import { neon } from '@neondatabase/serverless';
|
|
421
|
-
import dotenv from 'dotenv';
|
|
422
|
-
|
|
423
|
-
dotenv.config({ quiet: true });
|
|
424
|
-
|
|
425
|
-
const runMigrate = async () => {
|
|
426
|
-
if (!process.env.DATABASE_URL) {
|
|
427
|
-
throw new Error('DATABASE_URL is not defined');
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
const client = neon(process.env.DATABASE_URL);
|
|
431
|
-
const db = drizzle(client);
|
|
432
|
-
|
|
433
|
-
console.log('⏳ Running migrations...');
|
|
434
|
-
const start = Date.now();
|
|
435
|
-
await migrate(db, { migrationsFolder: './src/db/migrations' });
|
|
436
|
-
console.log('✅ Migrations completed in', Date.now() - start, 'ms');
|
|
437
|
-
process.exit(0);
|
|
438
|
-
};
|
|
439
|
-
|
|
440
|
-
runMigrate().catch((err) => {
|
|
441
|
-
console.error('❌ Migration failed');
|
|
442
|
-
console.error(err);
|
|
443
|
-
process.exit(1);
|
|
444
|
-
});
|
|
280
|
+
writeFile(path.join(dir, 'src/db/migrate.ts'), `import '@anhedral/db/migrate';
|
|
445
281
|
`);
|
|
446
282
|
}
|
|
447
283
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
@@ -1297,6 +1133,7 @@ const configPlugin: FastifyPluginAsync = async (fastify: FastifyInstance) => {
|
|
|
1297
1133
|
PORT: { type: 'number' },
|
|
1298
1134
|
NODE_ENV: { type: 'string', default: 'development' },
|
|
1299
1135
|
LOG_LEVEL: { type: 'string', default: 'info' },
|
|
1136
|
+
ANHEDRAL_DEMO: { type: 'string', default: 'false' },
|
|
1300
1137
|
CLERK_PUBLISHABLE_KEY: { type: 'string', nullable: true },
|
|
1301
1138
|
CLERK_SECRET_KEY: { type: 'string', nullable: true },
|
|
1302
1139
|
FRONTEND_URL: { type: 'string', nullable: true },
|
|
@@ -1366,6 +1203,15 @@ export const clerkAuthPlugin: FastifyPluginAsync = async (fastify: FastifyInstan
|
|
|
1366
1203
|
const authenticate = async (req: FastifyRequest, _reply: FastifyReply) => {
|
|
1367
1204
|
if (req.method === 'OPTIONS' || req.url.startsWith('/health')) return;
|
|
1368
1205
|
|
|
1206
|
+
if (fastify.env?.ANHEDRAL_DEMO === 'true') {
|
|
1207
|
+
req.user = {
|
|
1208
|
+
id: 'user_demo',
|
|
1209
|
+
subscriptionTier: 'pro',
|
|
1210
|
+
subscriptionStatus: 'active',
|
|
1211
|
+
};
|
|
1212
|
+
return;
|
|
1213
|
+
}
|
|
1214
|
+
|
|
1369
1215
|
const authHeader = req.headers.authorization;
|
|
1370
1216
|
if (!authHeader?.startsWith('Bearer ')) throw AuthError.unauthorized();
|
|
1371
1217
|
|
|
@@ -1929,6 +1775,23 @@ const authRoutes: FastifyPluginAsync = async (fastify: FastifyInstance) => {
|
|
|
1929
1775
|
}, async (req: FastifyRequest, reply: FastifyReply) => {
|
|
1930
1776
|
if (!req.user?.id) throw AuthError.unauthorized();
|
|
1931
1777
|
|
|
1778
|
+
if (fastify.env.ANHEDRAL_DEMO === 'true') {
|
|
1779
|
+
return reply.send({
|
|
1780
|
+
user: {
|
|
1781
|
+
id: 'user_demo',
|
|
1782
|
+
email: 'demo@anhedral.dev',
|
|
1783
|
+
firstName: 'Demo',
|
|
1784
|
+
lastName: 'Builder',
|
|
1785
|
+
displayName: '${displayName} Demo',
|
|
1786
|
+
imageUrl: null,
|
|
1787
|
+
avatarUrl: null,
|
|
1788
|
+
creditsBalance: 250,
|
|
1789
|
+
subscriptionTier: 'pro',
|
|
1790
|
+
subscriptionStatus: 'active',
|
|
1791
|
+
},
|
|
1792
|
+
});
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1932
1795
|
const clerkUser = await clerkClient.users.getUser(req.user.id);
|
|
1933
1796
|
const userData = await fastify.repos.users.getDashboardProfile(req.user.id);
|
|
1934
1797
|
const primaryEmail = clerkUser.emailAddresses.find(
|
|
@@ -2048,6 +1911,19 @@ const subscriptionRoutes: FastifyPluginAsync = async (fastify: FastifyInstance)
|
|
|
2048
1911
|
}, async (req, reply) => {
|
|
2049
1912
|
const userId = req.user?.id;
|
|
2050
1913
|
if (!userId) throw AuthError.unauthorized();
|
|
1914
|
+
if (app.env.ANHEDRAL_DEMO === 'true') {
|
|
1915
|
+
reply.header('Cache-Control', 'private, no-store');
|
|
1916
|
+
return reply.send({
|
|
1917
|
+
pro: true,
|
|
1918
|
+
inTrial: false,
|
|
1919
|
+
expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
1920
|
+
periodStart: new Date().toISOString(),
|
|
1921
|
+
periodEnd: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString(),
|
|
1922
|
+
method: 'paid',
|
|
1923
|
+
managementUrl: 'https://app.revenuecat.com/',
|
|
1924
|
+
cancelAtPeriodEnd: false,
|
|
1925
|
+
});
|
|
1926
|
+
}
|
|
2051
1927
|
const refreshRaw = (req.query as unknown as { refresh?: unknown }).refresh;
|
|
2052
1928
|
const requestedRefresh = refreshRaw === true || refreshRaw === 'true' || refreshRaw === 1 || refreshRaw === '1';
|
|
2053
1929
|
const forceRefresh = Boolean(app.env.RC_SECRET_API_KEY) && requestedRefresh;
|
|
@@ -2061,6 +1937,9 @@ const subscriptionRoutes: FastifyPluginAsync = async (fastify: FastifyInstance)
|
|
|
2061
1937
|
}, async (req, reply) => {
|
|
2062
1938
|
const userId = req.user?.id;
|
|
2063
1939
|
if (!userId) throw AuthError.unauthorized();
|
|
1940
|
+
if (app.env.ANHEDRAL_DEMO === 'true') {
|
|
1941
|
+
return { ok: true, expiresAt: new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString() };
|
|
1942
|
+
}
|
|
2064
1943
|
const { code } = req.body;
|
|
2065
1944
|
const validation = await app.repos.promoCodes.validateCode(code, userId);
|
|
2066
1945
|
if (!validation.valid || !validation.promoCode) {
|
|
@@ -2299,6 +2178,13 @@ async function getApp(): Promise<FastifyInstance> {
|
|
|
2299
2178
|
|
|
2300
2179
|
export default async function handler(req: VercelRequest, res: VercelResponse) {
|
|
2301
2180
|
const app = await getApp();
|
|
2181
|
+
if (req.url === '/api' || req.url === '/backend') {
|
|
2182
|
+
req.url = '/';
|
|
2183
|
+
} else if (req.url?.startsWith('/api/')) {
|
|
2184
|
+
req.url = req.url.slice('/api'.length);
|
|
2185
|
+
} else if (req.url?.startsWith('/backend/')) {
|
|
2186
|
+
req.url = req.url.slice('/backend'.length);
|
|
2187
|
+
}
|
|
2302
2188
|
app.server.emit('request', req, res);
|
|
2303
2189
|
}
|
|
2304
2190
|
`);
|