nextforge-cli 1.0.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/README.md +187 -0
- package/bin/cli.js +666 -0
- package/package.json +45 -0
- package/templates/auth/ldap/ldap-service.ts +146 -0
- package/templates/base/app/dashboard/page.tsx +97 -0
- package/templates/base/app/globals.css +59 -0
- package/templates/base/app/layout.tsx +27 -0
- package/templates/base/app/login/page.tsx +118 -0
- package/templates/base/app/page.tsx +48 -0
- package/templates/base/app/register/page.tsx +164 -0
- package/templates/base/components/ui/button.tsx +52 -0
- package/templates/base/components/ui/card.tsx +78 -0
- package/templates/base/components/ui/input.tsx +24 -0
- package/templates/base/components/ui/label.tsx +23 -0
- package/templates/base/features/auth/server/route.ts +160 -0
- package/templates/base/index.ts +75 -0
- package/templates/base/lib/auth.ts +74 -0
- package/templates/base/lib/logger.ts +66 -0
- package/templates/base/lib/prisma.ts +15 -0
- package/templates/base/lib/rpc.ts +35 -0
- package/templates/base/lib/store.ts +53 -0
- package/templates/base/lib/utils.ts +6 -0
- package/templates/base/middleware/auth-middleware.ts +42 -0
- package/templates/base/next.config.js +10 -0
- package/templates/base/postcss.config.js +6 -0
- package/templates/base/providers/providers.tsx +32 -0
- package/templates/base/tailwind.config.ts +58 -0
- package/templates/base/tsconfig.json +26 -0
- package/templates/email/email-service.ts +120 -0
- package/templates/stripe/route.ts +147 -0
- package/templates/stripe/stripe-service.ts +117 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Context, Next } from 'hono';
|
|
2
|
+
import { getCookie } from 'hono/cookie';
|
|
3
|
+
import { verifyToken, hasPermission, type UserRole, type JWTPayload } from '../lib/auth';
|
|
4
|
+
|
|
5
|
+
declare module 'hono' {
|
|
6
|
+
interface ContextVariableMap {
|
|
7
|
+
user: JWTPayload;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function authMiddleware(c: Context, next: Next) {
|
|
12
|
+
const token = getCookie(c, 'auth_token');
|
|
13
|
+
|
|
14
|
+
if (!token) {
|
|
15
|
+
return c.json({ success: false, error: 'Not authenticated' }, 401);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const payload = verifyToken(token);
|
|
19
|
+
|
|
20
|
+
if (!payload) {
|
|
21
|
+
return c.json({ success: false, error: 'Invalid token' }, 401);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
c.set('user', payload);
|
|
25
|
+
await next();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function requireRole(role: UserRole) {
|
|
29
|
+
return async (c: Context, next: Next) => {
|
|
30
|
+
const user = c.get('user');
|
|
31
|
+
|
|
32
|
+
if (!user) {
|
|
33
|
+
return c.json({ success: false, error: 'Not authenticated' }, 401);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (!hasPermission(user, role)) {
|
|
37
|
+
return c.json({ success: false, error: 'Insufficient permissions' }, 403);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
await next();
|
|
41
|
+
};
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
|
|
4
|
+
import { ThemeProvider } from 'next-themes'
|
|
5
|
+
import { useState } from 'react'
|
|
6
|
+
|
|
7
|
+
export function Providers({ children }: { children: React.ReactNode }) {
|
|
8
|
+
const [queryClient] = useState(
|
|
9
|
+
() =>
|
|
10
|
+
new QueryClient({
|
|
11
|
+
defaultOptions: {
|
|
12
|
+
queries: {
|
|
13
|
+
staleTime: 60 * 1000,
|
|
14
|
+
refetchOnWindowFocus: false,
|
|
15
|
+
},
|
|
16
|
+
},
|
|
17
|
+
})
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
return (
|
|
21
|
+
<QueryClientProvider client={queryClient}>
|
|
22
|
+
<ThemeProvider
|
|
23
|
+
attribute="class"
|
|
24
|
+
defaultTheme="system"
|
|
25
|
+
enableSystem
|
|
26
|
+
disableTransitionOnChange
|
|
27
|
+
>
|
|
28
|
+
{children}
|
|
29
|
+
</ThemeProvider>
|
|
30
|
+
</QueryClientProvider>
|
|
31
|
+
)
|
|
32
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Config } from "tailwindcss"
|
|
2
|
+
|
|
3
|
+
const config: Config = {
|
|
4
|
+
darkMode: ["class"],
|
|
5
|
+
content: [
|
|
6
|
+
"./pages/**/*.{js,ts,jsx,tsx,mdx}",
|
|
7
|
+
"./components/**/*.{js,ts,jsx,tsx,mdx}",
|
|
8
|
+
"./app/**/*.{js,ts,jsx,tsx,mdx}",
|
|
9
|
+
"./features/**/*.{js,ts,jsx,tsx,mdx}",
|
|
10
|
+
],
|
|
11
|
+
theme: {
|
|
12
|
+
extend: {
|
|
13
|
+
colors: {
|
|
14
|
+
border: "hsl(var(--border))",
|
|
15
|
+
input: "hsl(var(--input))",
|
|
16
|
+
ring: "hsl(var(--ring))",
|
|
17
|
+
background: "hsl(var(--background))",
|
|
18
|
+
foreground: "hsl(var(--foreground))",
|
|
19
|
+
primary: {
|
|
20
|
+
DEFAULT: "hsl(var(--primary))",
|
|
21
|
+
foreground: "hsl(var(--primary-foreground))",
|
|
22
|
+
},
|
|
23
|
+
secondary: {
|
|
24
|
+
DEFAULT: "hsl(var(--secondary))",
|
|
25
|
+
foreground: "hsl(var(--secondary-foreground))",
|
|
26
|
+
},
|
|
27
|
+
destructive: {
|
|
28
|
+
DEFAULT: "hsl(var(--destructive))",
|
|
29
|
+
foreground: "hsl(var(--destructive-foreground))",
|
|
30
|
+
},
|
|
31
|
+
muted: {
|
|
32
|
+
DEFAULT: "hsl(var(--muted))",
|
|
33
|
+
foreground: "hsl(var(--muted-foreground))",
|
|
34
|
+
},
|
|
35
|
+
accent: {
|
|
36
|
+
DEFAULT: "hsl(var(--accent))",
|
|
37
|
+
foreground: "hsl(var(--accent-foreground))",
|
|
38
|
+
},
|
|
39
|
+
popover: {
|
|
40
|
+
DEFAULT: "hsl(var(--popover))",
|
|
41
|
+
foreground: "hsl(var(--popover-foreground))",
|
|
42
|
+
},
|
|
43
|
+
card: {
|
|
44
|
+
DEFAULT: "hsl(var(--card))",
|
|
45
|
+
foreground: "hsl(var(--card-foreground))",
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
borderRadius: {
|
|
49
|
+
lg: "var(--radius)",
|
|
50
|
+
md: "calc(var(--radius) - 2px)",
|
|
51
|
+
sm: "calc(var(--radius) - 4px)",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
plugins: [require("tailwindcss-animate")],
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export default config
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["dom", "dom.iterable", "esnext"],
|
|
4
|
+
"allowJs": true,
|
|
5
|
+
"skipLibCheck": true,
|
|
6
|
+
"strict": true,
|
|
7
|
+
"noEmit": true,
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"module": "esnext",
|
|
10
|
+
"moduleResolution": "bundler",
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"isolatedModules": true,
|
|
13
|
+
"jsx": "preserve",
|
|
14
|
+
"incremental": true,
|
|
15
|
+
"plugins": [
|
|
16
|
+
{
|
|
17
|
+
"name": "next"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"paths": {
|
|
21
|
+
"@/*": ["./*"]
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
25
|
+
"exclude": ["node_modules"]
|
|
26
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import nodemailer from 'nodemailer';
|
|
2
|
+
import { logger } from '../logger';
|
|
3
|
+
|
|
4
|
+
interface EmailOptions {
|
|
5
|
+
to: string | string[];
|
|
6
|
+
subject: string;
|
|
7
|
+
text?: string;
|
|
8
|
+
html?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const transporter = nodemailer.createTransport({
|
|
12
|
+
host: process.env.SMTP_HOST || 'smtp.gmail.com',
|
|
13
|
+
port: parseInt(process.env.SMTP_PORT || '587'),
|
|
14
|
+
secure: process.env.SMTP_SECURE === 'true',
|
|
15
|
+
auth: process.env.SMTP_USER
|
|
16
|
+
? {
|
|
17
|
+
user: process.env.SMTP_USER,
|
|
18
|
+
pass: process.env.SMTP_PASSWORD,
|
|
19
|
+
}
|
|
20
|
+
: undefined,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export async function sendEmail(options: EmailOptions): Promise<boolean> {
|
|
24
|
+
try {
|
|
25
|
+
const from = `${process.env.EMAIL_FROM_NAME || 'App'} <${process.env.EMAIL_FROM || 'noreply@app.com'}>`;
|
|
26
|
+
|
|
27
|
+
await transporter.sendMail({
|
|
28
|
+
from,
|
|
29
|
+
to: Array.isArray(options.to) ? options.to.join(', ') : options.to,
|
|
30
|
+
subject: options.subject,
|
|
31
|
+
text: options.text,
|
|
32
|
+
html: options.html,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
logger.info('Email sent successfully', {
|
|
36
|
+
to: options.to,
|
|
37
|
+
subject: options.subject,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
} catch (error: any) {
|
|
42
|
+
logger.error('Failed to send email', {
|
|
43
|
+
error: error.message,
|
|
44
|
+
to: options.to,
|
|
45
|
+
subject: options.subject,
|
|
46
|
+
});
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Email templates
|
|
52
|
+
export function getWelcomeEmailHtml(name: string): string {
|
|
53
|
+
return `
|
|
54
|
+
<!DOCTYPE html>
|
|
55
|
+
<html>
|
|
56
|
+
<head>
|
|
57
|
+
<meta charset="utf-8">
|
|
58
|
+
<style>
|
|
59
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
60
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
61
|
+
.header { background: #4F46E5; color: white; padding: 20px; text-align: center; }
|
|
62
|
+
.content { padding: 20px; background: #f9f9f9; }
|
|
63
|
+
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
|
|
64
|
+
</style>
|
|
65
|
+
</head>
|
|
66
|
+
<body>
|
|
67
|
+
<div class="container">
|
|
68
|
+
<div class="header">
|
|
69
|
+
<h1>Welcome!</h1>
|
|
70
|
+
</div>
|
|
71
|
+
<div class="content">
|
|
72
|
+
<p>Hello ${name},</p>
|
|
73
|
+
<p>Welcome to our platform! We're excited to have you on board.</p>
|
|
74
|
+
<p>If you have any questions, feel free to reach out to our support team.</p>
|
|
75
|
+
</div>
|
|
76
|
+
<div class="footer">
|
|
77
|
+
<p>© ${new Date().getFullYear()} Your App. All rights reserved.</p>
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
</body>
|
|
81
|
+
</html>
|
|
82
|
+
`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function getPasswordResetEmailHtml(resetUrl: string): string {
|
|
86
|
+
return `
|
|
87
|
+
<!DOCTYPE html>
|
|
88
|
+
<html>
|
|
89
|
+
<head>
|
|
90
|
+
<meta charset="utf-8">
|
|
91
|
+
<style>
|
|
92
|
+
body { font-family: Arial, sans-serif; line-height: 1.6; color: #333; }
|
|
93
|
+
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
|
|
94
|
+
.header { background: #4F46E5; color: white; padding: 20px; text-align: center; }
|
|
95
|
+
.content { padding: 20px; background: #f9f9f9; }
|
|
96
|
+
.button { display: inline-block; padding: 12px 24px; background: #4F46E5; color: white; text-decoration: none; border-radius: 4px; }
|
|
97
|
+
.footer { text-align: center; padding: 20px; color: #666; font-size: 12px; }
|
|
98
|
+
</style>
|
|
99
|
+
</head>
|
|
100
|
+
<body>
|
|
101
|
+
<div class="container">
|
|
102
|
+
<div class="header">
|
|
103
|
+
<h1>Password Reset</h1>
|
|
104
|
+
</div>
|
|
105
|
+
<div class="content">
|
|
106
|
+
<p>You requested a password reset. Click the button below to reset your password:</p>
|
|
107
|
+
<p style="text-align: center;">
|
|
108
|
+
<a href="${resetUrl}" class="button">Reset Password</a>
|
|
109
|
+
</p>
|
|
110
|
+
<p>If you didn't request this, please ignore this email.</p>
|
|
111
|
+
<p>This link will expire in 1 hour.</p>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="footer">
|
|
114
|
+
<p>© ${new Date().getFullYear()} Your App. All rights reserved.</p>
|
|
115
|
+
</div>
|
|
116
|
+
</div>
|
|
117
|
+
</body>
|
|
118
|
+
</html>
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { Hono } from 'hono';
|
|
2
|
+
import { zValidator } from '@hono/zod-validator';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { authMiddleware } from '@/middleware/auth-middleware';
|
|
5
|
+
import { prisma } from '@/lib/prisma';
|
|
6
|
+
import { logger } from '@/lib/logger';
|
|
7
|
+
import {
|
|
8
|
+
createCheckoutSession,
|
|
9
|
+
createPortalSession,
|
|
10
|
+
handleWebhookEvent,
|
|
11
|
+
} from './stripe-service';
|
|
12
|
+
|
|
13
|
+
const app = new Hono();
|
|
14
|
+
|
|
15
|
+
const checkoutSchema = z.object({
|
|
16
|
+
priceId: z.string(),
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
// Create checkout session
|
|
20
|
+
app.post(
|
|
21
|
+
'/checkout',
|
|
22
|
+
authMiddleware,
|
|
23
|
+
zValidator('json', checkoutSchema),
|
|
24
|
+
async (c) => {
|
|
25
|
+
const user = c.get('user');
|
|
26
|
+
const { priceId } = c.req.valid('json');
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const dbUser = await prisma.user.findUnique({
|
|
30
|
+
where: { id: user.userId },
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (!dbUser) {
|
|
34
|
+
return c.json({ success: false, error: 'User not found' }, 404);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const appUrl = process.env.APP_URL || 'http://localhost:3001';
|
|
38
|
+
const session = await createCheckoutSession({
|
|
39
|
+
priceId,
|
|
40
|
+
userId: user.userId,
|
|
41
|
+
customerEmail: dbUser.email || undefined,
|
|
42
|
+
successUrl: `${appUrl}/billing?success=true`,
|
|
43
|
+
cancelUrl: `${appUrl}/billing?canceled=true`,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
return c.json({ success: true, data: { url: session.url } });
|
|
47
|
+
} catch (error: any) {
|
|
48
|
+
logger.error('Checkout error', { error: error.message });
|
|
49
|
+
return c.json({ success: false, error: 'Failed to create checkout' }, 500);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
|
|
54
|
+
// Create billing portal session
|
|
55
|
+
app.post('/portal', authMiddleware, async (c) => {
|
|
56
|
+
const user = c.get('user');
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const dbUser = await prisma.user.findUnique({
|
|
60
|
+
where: { id: user.userId },
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
if (!dbUser?.stripeCustomerId) {
|
|
64
|
+
return c.json({ success: false, error: 'No billing account found' }, 404);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const appUrl = process.env.APP_URL || 'http://localhost:3001';
|
|
68
|
+
const session = await createPortalSession({
|
|
69
|
+
customerId: dbUser.stripeCustomerId,
|
|
70
|
+
returnUrl: `${appUrl}/billing`,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return c.json({ success: true, data: { url: session.url } });
|
|
74
|
+
} catch (error: any) {
|
|
75
|
+
logger.error('Portal error', { error: error.message });
|
|
76
|
+
return c.json({ success: false, error: 'Failed to create portal' }, 500);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
// Webhook handler
|
|
81
|
+
app.post('/webhook', async (c) => {
|
|
82
|
+
const signature = c.req.header('stripe-signature');
|
|
83
|
+
|
|
84
|
+
if (!signature) {
|
|
85
|
+
return c.json({ error: 'Missing signature' }, 400);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
try {
|
|
89
|
+
const body = await c.req.text();
|
|
90
|
+
const event = await handleWebhookEvent(body, signature);
|
|
91
|
+
|
|
92
|
+
switch (event.type) {
|
|
93
|
+
case 'checkout.session.completed': {
|
|
94
|
+
const session = event.data.object;
|
|
95
|
+
const userId = parseInt(session.metadata?.userId || '0');
|
|
96
|
+
const customerId = session.customer as string;
|
|
97
|
+
|
|
98
|
+
if (userId) {
|
|
99
|
+
await prisma.user.update({
|
|
100
|
+
where: { id: userId },
|
|
101
|
+
data: { stripeCustomerId: customerId },
|
|
102
|
+
});
|
|
103
|
+
logger.info('Customer linked to user', { userId, customerId });
|
|
104
|
+
}
|
|
105
|
+
break;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
case 'customer.subscription.created':
|
|
109
|
+
case 'customer.subscription.updated': {
|
|
110
|
+
const subscription = event.data.object;
|
|
111
|
+
await prisma.subscription.upsert({
|
|
112
|
+
where: { stripeSubscriptionId: subscription.id },
|
|
113
|
+
create: {
|
|
114
|
+
stripeSubscriptionId: subscription.id,
|
|
115
|
+
userId: 0, // You'll need to look this up via customer ID
|
|
116
|
+
status: subscription.status,
|
|
117
|
+
priceId: subscription.items.data[0]?.price.id || '',
|
|
118
|
+
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
119
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
120
|
+
},
|
|
121
|
+
update: {
|
|
122
|
+
status: subscription.status,
|
|
123
|
+
currentPeriodStart: new Date(subscription.current_period_start * 1000),
|
|
124
|
+
currentPeriodEnd: new Date(subscription.current_period_end * 1000),
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
case 'customer.subscription.deleted': {
|
|
131
|
+
const subscription = event.data.object;
|
|
132
|
+
await prisma.subscription.update({
|
|
133
|
+
where: { stripeSubscriptionId: subscription.id },
|
|
134
|
+
data: { status: 'canceled' },
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return c.json({ received: true });
|
|
141
|
+
} catch (error: any) {
|
|
142
|
+
logger.error('Webhook error', { error: error.message });
|
|
143
|
+
return c.json({ error: 'Webhook error' }, 400);
|
|
144
|
+
}
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
export default app;
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import Stripe from 'stripe';
|
|
2
|
+
import { logger } from '@/lib/logger';
|
|
3
|
+
|
|
4
|
+
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
|
|
5
|
+
apiVersion: '2024-06-20',
|
|
6
|
+
});
|
|
7
|
+
|
|
8
|
+
export interface CreateCheckoutSessionParams {
|
|
9
|
+
priceId: string;
|
|
10
|
+
userId: number;
|
|
11
|
+
customerEmail?: string;
|
|
12
|
+
successUrl: string;
|
|
13
|
+
cancelUrl: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface CreatePortalSessionParams {
|
|
17
|
+
customerId: string;
|
|
18
|
+
returnUrl: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function createCheckoutSession(
|
|
22
|
+
params: CreateCheckoutSessionParams
|
|
23
|
+
): Promise<Stripe.Checkout.Session> {
|
|
24
|
+
try {
|
|
25
|
+
const session = await stripe.checkout.sessions.create({
|
|
26
|
+
mode: 'subscription',
|
|
27
|
+
payment_method_types: ['card'],
|
|
28
|
+
line_items: [
|
|
29
|
+
{
|
|
30
|
+
price: params.priceId,
|
|
31
|
+
quantity: 1,
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
success_url: params.successUrl,
|
|
35
|
+
cancel_url: params.cancelUrl,
|
|
36
|
+
customer_email: params.customerEmail,
|
|
37
|
+
metadata: {
|
|
38
|
+
userId: params.userId.toString(),
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
logger.info('Checkout session created', {
|
|
43
|
+
sessionId: session.id,
|
|
44
|
+
userId: params.userId,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return session;
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
logger.error('Failed to create checkout session', { error: error.message });
|
|
50
|
+
throw error;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export async function createPortalSession(
|
|
55
|
+
params: CreatePortalSessionParams
|
|
56
|
+
): Promise<Stripe.BillingPortal.Session> {
|
|
57
|
+
try {
|
|
58
|
+
const session = await stripe.billingPortal.sessions.create({
|
|
59
|
+
customer: params.customerId,
|
|
60
|
+
return_url: params.returnUrl,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
return session;
|
|
64
|
+
} catch (error: any) {
|
|
65
|
+
logger.error('Failed to create portal session', { error: error.message });
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function handleWebhookEvent(
|
|
71
|
+
body: string,
|
|
72
|
+
signature: string
|
|
73
|
+
): Promise<Stripe.Event> {
|
|
74
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET || '';
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
return stripe.webhooks.constructEvent(body, signature, webhookSecret);
|
|
78
|
+
} catch (error: any) {
|
|
79
|
+
logger.error('Webhook signature verification failed', {
|
|
80
|
+
error: error.message,
|
|
81
|
+
});
|
|
82
|
+
throw error;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export async function getCustomerSubscriptions(
|
|
87
|
+
customerId: string
|
|
88
|
+
): Promise<Stripe.Subscription[]> {
|
|
89
|
+
try {
|
|
90
|
+
const subscriptions = await stripe.subscriptions.list({
|
|
91
|
+
customer: customerId,
|
|
92
|
+
status: 'all',
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
return subscriptions.data;
|
|
96
|
+
} catch (error: any) {
|
|
97
|
+
logger.error('Failed to get subscriptions', { error: error.message });
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export async function cancelSubscription(
|
|
103
|
+
subscriptionId: string
|
|
104
|
+
): Promise<Stripe.Subscription> {
|
|
105
|
+
try {
|
|
106
|
+
const subscription = await stripe.subscriptions.cancel(subscriptionId);
|
|
107
|
+
|
|
108
|
+
logger.info('Subscription cancelled', { subscriptionId });
|
|
109
|
+
|
|
110
|
+
return subscription;
|
|
111
|
+
} catch (error: any) {
|
|
112
|
+
logger.error('Failed to cancel subscription', { error: error.message });
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { stripe };
|