@mars-stack/cli 0.2.0 → 1.0.2

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 (175) hide show
  1. package/dist/index.js +137 -12
  2. package/dist/index.js.map +1 -1
  3. package/package.json +4 -3
  4. package/template/.cursor/rules/composition-patterns.mdc +186 -0
  5. package/template/.cursor/rules/data-access.mdc +29 -0
  6. package/template/.cursor/rules/project-structure.mdc +34 -0
  7. package/template/.cursor/rules/security.mdc +25 -0
  8. package/template/.cursor/rules/testing.mdc +24 -0
  9. package/template/.cursor/rules/ui-conventions.mdc +29 -0
  10. package/template/.cursor/skills/add-api-route/SKILL.md +122 -0
  11. package/template/.cursor/skills/add-audit-log/SKILL.md +375 -0
  12. package/template/.cursor/skills/add-blog/SKILL.md +447 -0
  13. package/template/.cursor/skills/add-command-palette/SKILL.md +438 -0
  14. package/template/.cursor/skills/add-component/SKILL.md +158 -0
  15. package/template/.cursor/skills/add-crud-routes/SKILL.md +221 -0
  16. package/template/.cursor/skills/add-e2e-test/SKILL.md +227 -0
  17. package/template/.cursor/skills/add-error-boundary/SKILL.md +472 -0
  18. package/template/.cursor/skills/add-feature/SKILL.md +174 -0
  19. package/template/.cursor/skills/add-middleware/SKILL.md +135 -0
  20. package/template/.cursor/skills/add-page/SKILL.md +151 -0
  21. package/template/.cursor/skills/add-prisma-model/SKILL.md +148 -0
  22. package/template/.cursor/skills/add-protected-resource/SKILL.md +192 -0
  23. package/template/.cursor/skills/add-role/SKILL.md +156 -0
  24. package/template/.cursor/skills/add-server-action/SKILL.md +167 -0
  25. package/template/.cursor/skills/add-webhook/SKILL.md +192 -0
  26. package/template/.cursor/skills/build-complete-feature/SKILL.md +227 -0
  27. package/template/.cursor/skills/build-dashboard/SKILL.md +211 -0
  28. package/template/.cursor/skills/build-data-table/SKILL.md +283 -0
  29. package/template/.cursor/skills/build-form/SKILL.md +231 -0
  30. package/template/.cursor/skills/build-landing-page/SKILL.md +248 -0
  31. package/template/.cursor/skills/configure-ai/SKILL.md +617 -0
  32. package/template/.cursor/skills/configure-analytics/SKILL.md +413 -0
  33. package/template/.cursor/skills/configure-dark-mode/SKILL.md +309 -0
  34. package/template/.cursor/skills/configure-email/SKILL.md +170 -0
  35. package/template/.cursor/skills/configure-email-verification/SKILL.md +333 -0
  36. package/template/.cursor/skills/configure-feature-flags/SKILL.md +361 -0
  37. package/template/.cursor/skills/configure-i18n/SKILL.md +518 -0
  38. package/template/.cursor/skills/configure-jobs/SKILL.md +500 -0
  39. package/template/.cursor/skills/configure-magic-links/SKILL.md +385 -0
  40. package/template/.cursor/skills/configure-multi-tenancy/SKILL.md +611 -0
  41. package/template/.cursor/skills/configure-notifications/SKILL.md +569 -0
  42. package/template/.cursor/skills/configure-oauth/SKILL.md +217 -0
  43. package/template/.cursor/skills/configure-onboarding/SKILL.md +483 -0
  44. package/template/.cursor/skills/configure-payments/SKILL.md +243 -0
  45. package/template/.cursor/skills/configure-realtime/SKILL.md +733 -0
  46. package/template/.cursor/skills/configure-search/SKILL.md +581 -0
  47. package/template/.cursor/skills/configure-storage/SKILL.md +273 -0
  48. package/template/.cursor/skills/configure-two-factor/SKILL.md +518 -0
  49. package/template/.cursor/skills/create-execution-plan/SKILL.md +204 -0
  50. package/template/.cursor/skills/create-seed/SKILL.md +191 -0
  51. package/template/.cursor/skills/deploy-to-vercel/SKILL.md +300 -0
  52. package/template/.cursor/skills/design-tokens/SKILL.md +138 -0
  53. package/template/.cursor/skills/mars-capture-conversation-context/SKILL.md +119 -0
  54. package/template/.cursor/skills/setup-billing/SKILL.md +322 -0
  55. package/template/.cursor/skills/setup-project/SKILL.md +104 -0
  56. package/template/.cursor/skills/setup-teams/SKILL.md +682 -0
  57. package/template/.cursor/skills/test-api-route/SKILL.md +219 -0
  58. package/template/.cursor/skills/update-architecture-docs/SKILL.md +99 -0
  59. package/template/AGENTS.md +104 -0
  60. package/template/ARCHITECTURE.md +102 -0
  61. package/template/docs/QUALITY_SCORE.md +20 -0
  62. package/template/docs/design-docs/conversation-as-system-record.md +70 -0
  63. package/template/docs/design-docs/core-beliefs.md +43 -0
  64. package/template/docs/design-docs/index.md +8 -0
  65. package/template/docs/exec-plans/active/.gitkeep +0 -0
  66. package/template/docs/exec-plans/completed/.gitkeep +0 -0
  67. package/template/docs/exec-plans/tech-debt.md +7 -0
  68. package/template/docs/generated/.gitkeep +0 -0
  69. package/template/docs/product-specs/index.md +7 -0
  70. package/template/docs/references/index.md +18 -0
  71. package/template/e2e/api.spec.ts +20 -0
  72. package/template/e2e/auth.spec.ts +24 -0
  73. package/template/e2e/public.spec.ts +25 -0
  74. package/template/eslint.config.mjs +24 -0
  75. package/template/next-env.d.ts +6 -0
  76. package/template/next.config.ts +45 -0
  77. package/template/package.json +80 -0
  78. package/template/playwright.config.ts +31 -0
  79. package/template/postcss.config.mjs +8 -0
  80. package/template/prisma/generated/prisma/browser.ts +49 -0
  81. package/template/prisma/generated/prisma/client.ts +73 -0
  82. package/template/prisma/generated/prisma/commonInputTypes.ts +406 -0
  83. package/template/prisma/generated/prisma/enums.ts +15 -0
  84. package/template/prisma/generated/prisma/internal/class.ts +254 -0
  85. package/template/prisma/generated/prisma/internal/prismaNamespace.ts +1240 -0
  86. package/template/prisma/generated/prisma/internal/prismaNamespaceBrowser.ts +190 -0
  87. package/template/prisma/generated/prisma/models/Account.ts +1543 -0
  88. package/template/prisma/generated/prisma/models/File.ts +1529 -0
  89. package/template/prisma/generated/prisma/models/Session.ts +1415 -0
  90. package/template/prisma/generated/prisma/models/Subscription.ts +1455 -0
  91. package/template/prisma/generated/prisma/models/User.ts +2235 -0
  92. package/template/prisma/generated/prisma/models/VerificationToken.ts +1099 -0
  93. package/template/prisma/generated/prisma/models.ts +17 -0
  94. package/template/prisma/schema/auth.prisma +69 -0
  95. package/template/prisma/schema/base.prisma +8 -0
  96. package/template/prisma/schema/file.prisma +15 -0
  97. package/template/prisma/schema/subscription.prisma +17 -0
  98. package/template/prisma.config.ts +13 -0
  99. package/template/scripts/check-architecture.ts +221 -0
  100. package/template/scripts/check-doc-freshness.ts +242 -0
  101. package/template/scripts/ensure-db.mjs +291 -0
  102. package/template/scripts/generate-docs.ts +143 -0
  103. package/template/scripts/generate-env-example.ts +89 -0
  104. package/template/scripts/seed.ts +56 -0
  105. package/template/scripts/update-quality-score.ts +263 -0
  106. package/template/src/__tests__/architecture.test.ts +114 -0
  107. package/template/src/app/(auth)/forgotten-password/page.tsx +92 -0
  108. package/template/src/app/(auth)/layout.tsx +11 -0
  109. package/template/src/app/(auth)/register/page.tsx +162 -0
  110. package/template/src/app/(auth)/reset-password/page.tsx +109 -0
  111. package/template/src/app/(auth)/sign-in/page.tsx +122 -0
  112. package/template/src/app/(auth)/verify/[token]/page.tsx +87 -0
  113. package/template/src/app/(auth)/verify/page.tsx +56 -0
  114. package/template/src/app/(protected)/admin/page.tsx +108 -0
  115. package/template/src/app/(protected)/dashboard/loading.tsx +20 -0
  116. package/template/src/app/(protected)/dashboard/page.tsx +22 -0
  117. package/template/src/app/(protected)/layout.tsx +262 -0
  118. package/template/src/app/(protected)/settings/page.tsx +370 -0
  119. package/template/src/app/api/auth/forgot/route.ts +63 -0
  120. package/template/src/app/api/auth/login/route.ts +121 -0
  121. package/template/src/app/api/auth/logout/route.ts +19 -0
  122. package/template/src/app/api/auth/me/route.ts +30 -0
  123. package/template/src/app/api/auth/reset/route.ts +45 -0
  124. package/template/src/app/api/auth/signup/route.ts +85 -0
  125. package/template/src/app/api/auth/verify/route.ts +46 -0
  126. package/template/src/app/api/csrf/route.ts +12 -0
  127. package/template/src/app/api/health/route.ts +10 -0
  128. package/template/src/app/api/protected/admin/users/route.ts +24 -0
  129. package/template/src/app/api/protected/billing/checkout/route.ts +83 -0
  130. package/template/src/app/api/protected/billing/portal/route.ts +39 -0
  131. package/template/src/app/api/protected/files/[fileId]/route.ts +86 -0
  132. package/template/src/app/api/protected/files/upload/route.ts +64 -0
  133. package/template/src/app/api/protected/user/password/route.ts +63 -0
  134. package/template/src/app/api/protected/user/profile/route.ts +35 -0
  135. package/template/src/app/api/protected/user/sessions/[sessionId]/route.ts +33 -0
  136. package/template/src/app/api/protected/user/sessions/route.ts +22 -0
  137. package/template/src/app/api/readiness/route.ts +15 -0
  138. package/template/src/app/api/webhooks/stripe/route.ts +166 -0
  139. package/template/src/app/error.tsx +33 -0
  140. package/template/src/app/layout.tsx +29 -0
  141. package/template/src/app/not-found.tsx +20 -0
  142. package/template/src/app/page.tsx +136 -0
  143. package/template/src/app/privacy/page.tsx +178 -0
  144. package/template/src/app/providers.tsx +8 -0
  145. package/template/src/app/terms/page.tsx +139 -0
  146. package/template/src/config/app.config.ts +70 -0
  147. package/template/src/config/routes.ts +17 -0
  148. package/template/src/features/admin/index.ts +11 -0
  149. package/template/src/features/admin/permissions.ts +64 -0
  150. package/template/src/features/auth/context/AuthContext.tsx +96 -0
  151. package/template/src/features/auth/context/index.ts +2 -0
  152. package/template/src/features/auth/index.ts +3 -0
  153. package/template/src/features/auth/server/consent.ts +66 -0
  154. package/template/src/features/auth/server/session-revocation.ts +20 -0
  155. package/template/src/features/auth/server/sessions.ts +66 -0
  156. package/template/src/features/auth/server/user.ts +166 -0
  157. package/template/src/features/auth/types.ts +19 -0
  158. package/template/src/features/auth/validators.ts +29 -0
  159. package/template/src/features/billing/server/index.ts +66 -0
  160. package/template/src/features/billing/types.ts +43 -0
  161. package/template/src/features/uploads/server/index.ts +49 -0
  162. package/template/src/features/uploads/types.ts +26 -0
  163. package/template/src/lib/core/email/templates/base-layout.ts +122 -0
  164. package/template/src/lib/core/email/templates/index.ts +4 -0
  165. package/template/src/lib/core/email/templates/password-reset-email.ts +42 -0
  166. package/template/src/lib/core/email/templates/verification-email.ts +41 -0
  167. package/template/src/lib/core/email/templates/welcome-email.ts +40 -0
  168. package/template/src/lib/mars.ts +56 -0
  169. package/template/src/lib/prisma.ts +19 -0
  170. package/template/src/proxy.ts +92 -0
  171. package/template/src/styles/brand.css +15 -0
  172. package/template/src/styles/globals.css +7 -0
  173. package/template/tsconfig.json +59 -0
  174. package/template/vitest.config.ts +41 -0
  175. package/template/vitest.setup.ts +24 -0
@@ -0,0 +1,63 @@
1
+ import { withAuthNoParams, handleApiError } from '@/lib/mars';
2
+ import { prisma } from '@/lib/prisma';
3
+ import type { AuthenticatedRequest } from '@mars-stack/core/auth/middleware';
4
+ import { verifyPassword, hashPassword, passwordSchema } from '@mars-stack/core/auth/password';
5
+ import { NextResponse } from 'next/server';
6
+ import { z } from 'zod';
7
+
8
+ const changePasswordSchema = z.object({
9
+ currentPassword: z.string().min(1, 'Current password is required'),
10
+ newPassword: passwordSchema,
11
+ });
12
+
13
+ export const POST = withAuthNoParams(async (request: AuthenticatedRequest) => {
14
+ try {
15
+ const body = await request.json();
16
+ const result = changePasswordSchema.safeParse(body);
17
+
18
+ if (!result.success) {
19
+ return NextResponse.json(
20
+ { error: result.error.issues[0]?.message || 'Invalid input' },
21
+ { status: 400 },
22
+ );
23
+ }
24
+
25
+ const { currentPassword, newPassword } = result.data;
26
+
27
+ const user = await prisma.user.findUnique({
28
+ where: { id: request.session.userId },
29
+ select: { password: true },
30
+ });
31
+
32
+ if (!user?.password) {
33
+ return NextResponse.json(
34
+ { error: 'Unable to verify current password' },
35
+ { status: 400 },
36
+ );
37
+ }
38
+
39
+ const isValid = await verifyPassword(currentPassword, user.password);
40
+ if (!isValid) {
41
+ return NextResponse.json(
42
+ { error: 'Current password is incorrect' },
43
+ { status: 401 },
44
+ );
45
+ }
46
+
47
+ const hashedPassword = await hashPassword(newPassword);
48
+
49
+ await prisma.user.update({
50
+ where: { id: request.session.userId },
51
+ data: {
52
+ password: hashedPassword,
53
+ failedLoginAttempts: 0,
54
+ lastFailedLogin: null,
55
+ lockedUntil: null,
56
+ },
57
+ });
58
+
59
+ return NextResponse.json({ message: 'Password updated successfully' });
60
+ } catch (error) {
61
+ return handleApiError(error, { endpoint: '/api/protected/user/password' });
62
+ }
63
+ });
@@ -0,0 +1,35 @@
1
+ import { withAuthNoParams, handleApiError } from '@/lib/mars';
2
+ import { prisma } from '@/lib/prisma';
3
+ import type { AuthenticatedRequest } from '@mars-stack/core/auth/middleware';
4
+ import { NextResponse } from 'next/server';
5
+ import { z } from 'zod';
6
+
7
+ const updateProfileSchema = z.object({
8
+ name: z.string().min(1, 'Name is required').max(100, 'Name is too long').trim(),
9
+ });
10
+
11
+ export const PATCH = withAuthNoParams(async (request: AuthenticatedRequest) => {
12
+ try {
13
+ const body = await request.json();
14
+ const result = updateProfileSchema.safeParse(body);
15
+
16
+ if (!result.success) {
17
+ return NextResponse.json(
18
+ { error: result.error.issues[0]?.message || 'Invalid input' },
19
+ { status: 400 },
20
+ );
21
+ }
22
+
23
+ const { name } = result.data;
24
+
25
+ const user = await prisma.user.update({
26
+ where: { id: request.session.userId },
27
+ data: { name },
28
+ select: { id: true, name: true, email: true, role: true },
29
+ });
30
+
31
+ return NextResponse.json({ user });
32
+ } catch (error) {
33
+ return handleApiError(error, { endpoint: '/api/protected/user/profile' });
34
+ }
35
+ });
@@ -0,0 +1,33 @@
1
+ import { withAuth, handleApiError } from '@/lib/mars';
2
+ import { findSessionById, revokeSession } from '@/features/auth/server/sessions';
3
+ import type { AuthenticatedRequest } from '@mars-stack/core/auth/middleware';
4
+ import { NextResponse } from 'next/server';
5
+
6
+ export const DELETE = withAuth(
7
+ async (
8
+ request: AuthenticatedRequest,
9
+ context: { params: Promise<{ [key: string]: string }> },
10
+ ) => {
11
+ try {
12
+ const { sessionId } = await context.params;
13
+
14
+ const session = await findSessionById(sessionId);
15
+
16
+ if (!session) {
17
+ return NextResponse.json({ error: 'Session not found' }, { status: 404 });
18
+ }
19
+
20
+ if (session.userId !== request.session.userId) {
21
+ return NextResponse.json({ error: 'Forbidden' }, { status: 403 });
22
+ }
23
+
24
+ await revokeSession(sessionId);
25
+
26
+ return NextResponse.json({ message: 'Session revoked' });
27
+ } catch (error) {
28
+ return handleApiError(error, {
29
+ endpoint: '/api/protected/user/sessions/[sessionId]',
30
+ });
31
+ }
32
+ },
33
+ );
@@ -0,0 +1,22 @@
1
+ import { withAuthNoParams, handleApiError } from '@/lib/mars';
2
+ import { listSessionsForUser, revokeAllSessionsForUser } from '@/features/auth/server/sessions';
3
+ import type { AuthenticatedRequest } from '@mars-stack/core/auth/middleware';
4
+ import { NextResponse } from 'next/server';
5
+
6
+ export const GET = withAuthNoParams(async (request: AuthenticatedRequest) => {
7
+ try {
8
+ const sessions = await listSessionsForUser(request.session.userId);
9
+ return NextResponse.json({ sessions });
10
+ } catch (error) {
11
+ return handleApiError(error, { endpoint: '/api/protected/user/sessions' });
12
+ }
13
+ });
14
+
15
+ export const DELETE = withAuthNoParams(async (request: AuthenticatedRequest) => {
16
+ try {
17
+ await revokeAllSessionsForUser(request.session.userId);
18
+ return NextResponse.json({ message: 'All sessions revoked' });
19
+ } catch (error) {
20
+ return handleApiError(error, { endpoint: '/api/protected/user/sessions' });
21
+ }
22
+ });
@@ -0,0 +1,15 @@
1
+ import { prisma } from '@/lib/prisma';
2
+ import { NextResponse } from 'next/server';
3
+
4
+ export async function GET() {
5
+ try {
6
+ await prisma.$queryRaw`SELECT 1`;
7
+
8
+ return NextResponse.json({ status: 'ready', db: 'connected' });
9
+ } catch {
10
+ return NextResponse.json(
11
+ { status: 'not_ready', db: 'error' },
12
+ { status: 503 },
13
+ );
14
+ }
15
+ }
@@ -0,0 +1,166 @@
1
+ import { createPaymentService } from '@mars-stack/core/payments';
2
+ import { appConfig } from '@/config/app.config';
3
+ import { prisma } from '@/lib/prisma';
4
+ import { NextResponse } from 'next/server';
5
+ import type {
6
+ UpsertSubscriptionData,
7
+ UpdateSubscriptionStatusData,
8
+ } from '@/features/billing/types';
9
+
10
+ const payments = createPaymentService({
11
+ provider: appConfig.services.payments.provider,
12
+ });
13
+
14
+ interface StripeSubscription {
15
+ id: string;
16
+ customer: string;
17
+ status: string;
18
+ items: { data: Array<{ price: { id: string } }> };
19
+ current_period_end: number;
20
+ cancel_at_period_end: boolean;
21
+ }
22
+
23
+ interface StripeCheckoutSession {
24
+ customer: string;
25
+ subscription: string;
26
+ metadata: Record<string, string> | null;
27
+ }
28
+
29
+ interface StripeEvent {
30
+ type: string;
31
+ data: {
32
+ object: StripeSubscription | StripeCheckoutSession;
33
+ };
34
+ }
35
+
36
+ async function handleCheckoutCompleted(session: StripeCheckoutSession): Promise<void> {
37
+ const userId = session.metadata?.userId;
38
+ if (!userId) {
39
+ console.error('[Stripe Webhook] checkout.session.completed missing userId in metadata');
40
+ return;
41
+ }
42
+
43
+ const Stripe = (await import('stripe')).default;
44
+ const secretKey = process.env.STRIPE_SECRET_KEY;
45
+ if (!secretKey) throw new Error('STRIPE_SECRET_KEY is not set');
46
+
47
+ const stripe = new Stripe(secretKey);
48
+ const subscription = await stripe.subscriptions.retrieve(session.subscription);
49
+
50
+ const data: UpsertSubscriptionData = {
51
+ userId,
52
+ stripeCustomerId: session.customer,
53
+ stripeSubscriptionId: subscription.id,
54
+ stripePriceId: subscription.items.data[0]?.price.id,
55
+ status: subscription.status,
56
+ currentPeriodEnd: new Date(subscription.current_period_end * 1000),
57
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
58
+ };
59
+
60
+ await prisma.subscription.upsert({
61
+ where: { userId },
62
+ create: {
63
+ userId: data.userId,
64
+ stripeCustomerId: data.stripeCustomerId,
65
+ stripeSubscriptionId: data.stripeSubscriptionId ?? null,
66
+ stripePriceId: data.stripePriceId ?? null,
67
+ status: data.status,
68
+ currentPeriodEnd: data.currentPeriodEnd ?? null,
69
+ cancelAtPeriodEnd: data.cancelAtPeriodEnd ?? false,
70
+ },
71
+ update: {
72
+ stripeCustomerId: data.stripeCustomerId,
73
+ stripeSubscriptionId: data.stripeSubscriptionId,
74
+ stripePriceId: data.stripePriceId,
75
+ status: data.status,
76
+ currentPeriodEnd: data.currentPeriodEnd,
77
+ cancelAtPeriodEnd: data.cancelAtPeriodEnd,
78
+ },
79
+ });
80
+ }
81
+
82
+ async function handleSubscriptionUpdated(subscription: StripeSubscription): Promise<void> {
83
+ const existing = await prisma.subscription.findUnique({
84
+ where: { stripeSubscriptionId: subscription.id },
85
+ });
86
+
87
+ if (!existing) {
88
+ console.warn(
89
+ `[Stripe Webhook] subscription.updated for unknown subscription: ${subscription.id}`,
90
+ );
91
+ return;
92
+ }
93
+
94
+ const data: UpdateSubscriptionStatusData = {
95
+ status: subscription.status,
96
+ stripePriceId: subscription.items.data[0]?.price.id,
97
+ currentPeriodEnd: new Date(subscription.current_period_end * 1000),
98
+ cancelAtPeriodEnd: subscription.cancel_at_period_end,
99
+ };
100
+
101
+ await prisma.subscription.update({
102
+ where: { stripeSubscriptionId: subscription.id },
103
+ data: {
104
+ status: data.status,
105
+ stripePriceId: data.stripePriceId,
106
+ currentPeriodEnd: data.currentPeriodEnd,
107
+ cancelAtPeriodEnd: data.cancelAtPeriodEnd,
108
+ },
109
+ });
110
+ }
111
+
112
+ async function handleSubscriptionDeleted(subscription: StripeSubscription): Promise<void> {
113
+ const existing = await prisma.subscription.findUnique({
114
+ where: { stripeSubscriptionId: subscription.id },
115
+ });
116
+
117
+ if (!existing) {
118
+ console.warn(
119
+ `[Stripe Webhook] subscription.deleted for unknown subscription: ${subscription.id}`,
120
+ );
121
+ return;
122
+ }
123
+
124
+ await prisma.subscription.update({
125
+ where: { stripeSubscriptionId: subscription.id },
126
+ data: {
127
+ status: 'canceled',
128
+ cancelAtPeriodEnd: false,
129
+ },
130
+ });
131
+ }
132
+
133
+ export async function POST(request: Request): Promise<NextResponse> {
134
+ try {
135
+ const body = await request.text();
136
+ const signature = request.headers.get('stripe-signature');
137
+
138
+ if (!signature) {
139
+ return NextResponse.json({ error: 'Missing stripe-signature header' }, { status: 400 });
140
+ }
141
+
142
+ const event = (await payments.constructWebhookEvent(body, signature)) as StripeEvent;
143
+
144
+ switch (event.type) {
145
+ case 'checkout.session.completed':
146
+ await handleCheckoutCompleted(event.data.object as StripeCheckoutSession);
147
+ break;
148
+ case 'customer.subscription.updated':
149
+ await handleSubscriptionUpdated(event.data.object as StripeSubscription);
150
+ break;
151
+ case 'customer.subscription.deleted':
152
+ await handleSubscriptionDeleted(event.data.object as StripeSubscription);
153
+ break;
154
+ default:
155
+ break;
156
+ }
157
+
158
+ return NextResponse.json({ received: true });
159
+ } catch (error) {
160
+ console.error('[Stripe Webhook] Error processing event:', error);
161
+ return NextResponse.json(
162
+ { error: 'Webhook processing failed' },
163
+ { status: 400 },
164
+ );
165
+ }
166
+ }
@@ -0,0 +1,33 @@
1
+ 'use client';
2
+
3
+ import { appConfig } from '@/config/app.config';
4
+
5
+ interface ErrorPageProps {
6
+ error: Error & { digest?: string };
7
+ reset: () => void;
8
+ }
9
+
10
+ export default function ErrorPage({ error, reset }: ErrorPageProps) {
11
+ return (
12
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4">
13
+ <p className="text-6xl font-bold text-text-muted">500</p>
14
+ <h1 className="text-2xl font-semibold text-text-primary">Something went wrong</h1>
15
+ <p className="max-w-md text-center text-text-secondary">
16
+ An unexpected error occurred. Please try again, or contact{' '}
17
+ <a href={`mailto:${appConfig.support.email}`} className="text-text-link hover:underline">
18
+ support
19
+ </a>{' '}
20
+ if the problem persists.
21
+ </p>
22
+ {error.digest && (
23
+ <p className="text-xs text-text-muted">Error reference: {error.digest}</p>
24
+ )}
25
+ <button
26
+ onClick={reset}
27
+ className="mt-2 rounded-lg bg-brand-primary px-6 py-3 text-sm font-medium text-text-inverse transition-colors hover:bg-brand-primary-hover"
28
+ >
29
+ Try again
30
+ </button>
31
+ </main>
32
+ );
33
+ }
@@ -0,0 +1,29 @@
1
+ import type { Metadata } from 'next';
2
+ import { Inter } from 'next/font/google';
3
+ import '@/styles/globals.css';
4
+ import { appConfig } from '@/config/app.config';
5
+ import { Providers } from './providers';
6
+
7
+ const inter = Inter({
8
+ subsets: ['latin'],
9
+ variable: '--font-sans',
10
+ });
11
+
12
+ export const metadata: Metadata = {
13
+ title: {
14
+ default: appConfig.name,
15
+ template: `%s | ${appConfig.name}`,
16
+ },
17
+ description: appConfig.description,
18
+ metadataBase: new URL(appConfig.url),
19
+ };
20
+
21
+ export default function RootLayout({ children }: { children: React.ReactNode }) {
22
+ return (
23
+ <html lang="en" className={inter.variable} suppressHydrationWarning>
24
+ <body className="bg-surface-background text-text-primary antialiased">
25
+ <Providers>{children}</Providers>
26
+ </body>
27
+ </html>
28
+ );
29
+ }
@@ -0,0 +1,20 @@
1
+ import Link from 'next/link';
2
+ import { appConfig } from '@/config/app.config';
3
+
4
+ export default function NotFound() {
5
+ return (
6
+ <main className="flex min-h-screen flex-col items-center justify-center gap-4 px-4">
7
+ <p className="text-6xl font-bold text-text-muted">404</p>
8
+ <h1 className="text-2xl font-semibold text-text-primary">Page not found</h1>
9
+ <p className="max-w-md text-center text-text-secondary">
10
+ The page you&apos;re looking for doesn&apos;t exist or has been moved.
11
+ </p>
12
+ <Link
13
+ href="/"
14
+ className="mt-2 rounded-lg bg-brand-primary px-6 py-3 text-sm font-medium text-text-inverse transition-colors hover:bg-brand-primary-hover"
15
+ >
16
+ Go back to {appConfig.name}
17
+ </Link>
18
+ </main>
19
+ );
20
+ }
@@ -0,0 +1,136 @@
1
+ 'use client';
2
+
3
+ import { LinkButton } from '@mars-stack/ui';
4
+ import Link from 'next/link';
5
+ import { appConfig } from '@/config/app.config';
6
+ import { routes } from '@/config/routes';
7
+
8
+ const features = [
9
+ {
10
+ title: 'Authentication Built In',
11
+ description:
12
+ 'Email/password auth, email verification, password reset, and session management — ready from day one.',
13
+ icon: (
14
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
15
+ <path strokeLinecap="round" strokeLinejoin="round" d="M16.5 10.5V6.75a4.5 4.5 0 1 0-9 0v3.75m-.75 11.25h10.5a2.25 2.25 0 0 0 2.25-2.25v-6.75a2.25 2.25 0 0 0-2.25-2.25H6.75a2.25 2.25 0 0 0-2.25 2.25v6.75a2.25 2.25 0 0 0 2.25 2.25Z" />
16
+ </svg>
17
+ ),
18
+ },
19
+ {
20
+ title: 'Design Token System',
21
+ description:
22
+ 'Semantic color tokens, typography scale, and spacing primitives that adapt to light and dark mode automatically.',
23
+ icon: (
24
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
25
+ <path strokeLinecap="round" strokeLinejoin="round" d="M4.098 19.902a3.75 3.75 0 0 0 5.304 0l6.401-6.402M6.75 21A3.75 3.75 0 0 1 3 17.25V4.125C3 3.504 3.504 3 4.125 3h5.25c.621 0 1.125.504 1.125 1.125v4.072M6.75 21a3.75 3.75 0 0 0 3.75-3.75V8.197M6.75 21h13.125c.621 0 1.125-.504 1.125-1.125v-5.25c0-.621-.504-1.125-1.125-1.125h-4.072M10.5 8.197l2.88-2.88c.438-.439 1.15-.439 1.59 0l3.712 3.713c.44.44.44 1.152 0 1.59l-2.879 2.88M6.75 17.25h.008v.008H6.75v-.008Z" />
26
+ </svg>
27
+ ),
28
+ },
29
+ {
30
+ title: 'Role-Based Access',
31
+ description:
32
+ 'Admin panel, role verification on every API request, and ownership-gated routes to keep your data secure.',
33
+ icon: (
34
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
35
+ <path strokeLinecap="round" strokeLinejoin="round" d="M9 12.75 11.25 15 15 9.75m-3-7.036A11.959 11.959 0 0 1 3.598 6 11.99 11.99 0 0 0 3 9.749c0 5.592 3.824 10.29 9 11.623 5.176-1.332 9-6.03 9-11.622 0-1.31-.21-2.571-.598-3.751h-.152c-3.196 0-6.1-1.248-8.25-3.285Z" />
36
+ </svg>
37
+ ),
38
+ },
39
+ {
40
+ title: 'Production Ready',
41
+ description:
42
+ 'Structured logging, rate limiting, CSRF protection, and serverless-friendly patterns out of the box.',
43
+ icon: (
44
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" strokeWidth={1.5} stroke="currentColor" className="size-6">
45
+ <path strokeLinecap="round" strokeLinejoin="round" d="M3.75 13.5l10.5-11.25L12 10.5h8.25L9.75 21.75 12 13.5H3.75Z" />
46
+ </svg>
47
+ ),
48
+ },
49
+ ];
50
+
51
+ export default function HomePage() {
52
+ return (
53
+ <div className="flex min-h-screen flex-col bg-surface-background">
54
+ {/* Hero */}
55
+ <main className="flex flex-1 flex-col items-center justify-center px-6 py-24 text-center">
56
+ <span className="mb-6 inline-block rounded-full border border-border-default bg-surface-card px-4 py-1.5 text-sm font-medium text-text-secondary">
57
+ {appConfig.description}
58
+ </span>
59
+
60
+ <h1 className="mb-6 max-w-2xl text-5xl font-bold leading-[1.1] tracking-tight text-text-primary sm:text-6xl">
61
+ {appConfig.name}
62
+ </h1>
63
+
64
+ <p className="mx-auto mb-10 max-w-lg text-lg leading-relaxed text-text-secondary">
65
+ {appConfig.tagline}
66
+ </p>
67
+
68
+ <div className="flex flex-wrap items-center justify-center gap-4">
69
+ <LinkButton href={routes.signUp} size="lg">
70
+ Get Started
71
+ </LinkButton>
72
+ <LinkButton href={routes.signIn} variant="secondary" size="lg">
73
+ Sign In
74
+ </LinkButton>
75
+ </div>
76
+ </main>
77
+
78
+ {/* Features */}
79
+ <section className="border-t border-border-default bg-surface-card px-6 py-24">
80
+ <div className="mx-auto max-w-5xl">
81
+ <h2 className="mb-4 text-center text-3xl font-bold text-text-primary">Everything you need to ship</h2>
82
+ <p className="mx-auto mb-16 max-w-xl text-center text-base leading-relaxed text-text-secondary">
83
+ Stop rebuilding the same infrastructure. Start with a production-grade foundation and
84
+ focus on what makes your product unique.
85
+ </p>
86
+
87
+ <div className="grid gap-8 sm:grid-cols-2">
88
+ {features.map((feature) => (
89
+ <div
90
+ key={feature.title}
91
+ className="rounded-2xl border border-border-default bg-surface-background p-8 transition-shadow hover:shadow-md"
92
+ >
93
+ <div className="mb-4 flex size-12 items-center justify-center rounded-xl bg-brand-primary-muted text-brand-primary">
94
+ {feature.icon}
95
+ </div>
96
+ <h3 className="mb-2 text-lg font-semibold text-text-primary">{feature.title}</h3>
97
+ <p className="text-base leading-relaxed text-text-secondary">{feature.description}</p>
98
+ </div>
99
+ ))}
100
+ </div>
101
+ </div>
102
+ </section>
103
+
104
+ {/* Footer */}
105
+ <footer className="border-t border-border-default bg-surface-background px-6 py-12">
106
+ <div className="mx-auto flex max-w-5xl flex-col items-center gap-6 sm:flex-row sm:justify-between">
107
+ <p className="text-sm text-text-muted">
108
+ &copy; {new Date().getFullYear()}{' '}
109
+ {appConfig.legal.companyName || appConfig.name}. All rights reserved.
110
+ </p>
111
+
112
+ <nav className="flex items-center gap-6">
113
+ <Link
114
+ href={routes.terms}
115
+ className="text-sm text-text-secondary transition-colors hover:text-text-primary"
116
+ >
117
+ Terms
118
+ </Link>
119
+ <Link
120
+ href={routes.privacy}
121
+ className="text-sm text-text-secondary transition-colors hover:text-text-primary"
122
+ >
123
+ Privacy
124
+ </Link>
125
+ <a
126
+ href={`mailto:${appConfig.support.email}`}
127
+ className="text-sm text-text-secondary transition-colors hover:text-text-primary"
128
+ >
129
+ Contact
130
+ </a>
131
+ </nav>
132
+ </div>
133
+ </footer>
134
+ </div>
135
+ );
136
+ }