mcp-twin 1.2.0 → 1.4.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/.env.example +30 -0
- package/PRD.md +682 -0
- package/dist/cli.js +41 -0
- package/dist/cli.js.map +1 -1
- package/dist/cloud/auth.d.ts +108 -0
- package/dist/cloud/auth.d.ts.map +1 -0
- package/dist/cloud/auth.js +199 -0
- package/dist/cloud/auth.js.map +1 -0
- package/dist/cloud/db.d.ts +21 -0
- package/dist/cloud/db.d.ts.map +1 -0
- package/dist/cloud/db.js +158 -0
- package/dist/cloud/db.js.map +1 -0
- package/dist/cloud/routes/auth.d.ts +7 -0
- package/dist/cloud/routes/auth.d.ts.map +1 -0
- package/dist/cloud/routes/auth.js +291 -0
- package/dist/cloud/routes/auth.js.map +1 -0
- package/dist/cloud/routes/billing.d.ts +7 -0
- package/dist/cloud/routes/billing.d.ts.map +1 -0
- package/dist/cloud/routes/billing.js +368 -0
- package/dist/cloud/routes/billing.js.map +1 -0
- package/dist/cloud/routes/twins.d.ts +7 -0
- package/dist/cloud/routes/twins.d.ts.map +1 -0
- package/dist/cloud/routes/twins.js +740 -0
- package/dist/cloud/routes/twins.js.map +1 -0
- package/dist/cloud/routes/usage.d.ts +7 -0
- package/dist/cloud/routes/usage.d.ts.map +1 -0
- package/dist/cloud/routes/usage.js +145 -0
- package/dist/cloud/routes/usage.js.map +1 -0
- package/dist/cloud/server.d.ts +10 -0
- package/dist/cloud/server.d.ts.map +1 -0
- package/dist/cloud/server.js +161 -0
- package/dist/cloud/server.js.map +1 -0
- package/dist/cloud/stripe.d.ts +60 -0
- package/dist/cloud/stripe.d.ts.map +1 -0
- package/dist/cloud/stripe.js +157 -0
- package/dist/cloud/stripe.js.map +1 -0
- package/package.json +25 -4
- package/src/cli.ts +10 -0
- package/src/cloud/auth.ts +269 -0
- package/src/cloud/db.ts +167 -0
- package/src/cloud/routes/auth.ts +355 -0
- package/src/cloud/routes/billing.ts +460 -0
- package/src/cloud/routes/twins.ts +908 -0
- package/src/cloud/routes/usage.ts +186 -0
- package/src/cloud/server.ts +171 -0
- package/src/cloud/stripe.ts +192 -0
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Auth Routes
|
|
4
|
+
* MCP Twin Cloud
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const db_1 = require("../db");
|
|
9
|
+
const auth_1 = require("../auth");
|
|
10
|
+
const router = (0, express_1.Router)();
|
|
11
|
+
/**
|
|
12
|
+
* POST /api/auth/signup
|
|
13
|
+
* Create a new account
|
|
14
|
+
*/
|
|
15
|
+
router.post('/signup', async (req, res) => {
|
|
16
|
+
try {
|
|
17
|
+
const { email, password } = req.body;
|
|
18
|
+
// Validate input
|
|
19
|
+
if (!email || !password) {
|
|
20
|
+
res.status(400).json({
|
|
21
|
+
error: {
|
|
22
|
+
code: 'VALIDATION_ERROR',
|
|
23
|
+
message: 'Email and password are required',
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (password.length < 8) {
|
|
29
|
+
res.status(400).json({
|
|
30
|
+
error: {
|
|
31
|
+
code: 'VALIDATION_ERROR',
|
|
32
|
+
message: 'Password must be at least 8 characters',
|
|
33
|
+
},
|
|
34
|
+
});
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
// Check if user exists
|
|
38
|
+
const existing = await (0, db_1.queryOne)('SELECT id FROM users WHERE email = $1', [email.toLowerCase()]);
|
|
39
|
+
if (existing) {
|
|
40
|
+
res.status(409).json({
|
|
41
|
+
error: {
|
|
42
|
+
code: 'USER_EXISTS',
|
|
43
|
+
message: 'An account with this email already exists',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// Create user
|
|
49
|
+
const passwordHash = await (0, auth_1.hashPassword)(password);
|
|
50
|
+
const users = await (0, db_1.query)(`INSERT INTO users (email, password_hash)
|
|
51
|
+
VALUES ($1, $2)
|
|
52
|
+
RETURNING id, email, tier, created_at`, [email.toLowerCase(), passwordHash]);
|
|
53
|
+
const user = users[0];
|
|
54
|
+
// Generate initial API key
|
|
55
|
+
const { key, hash, prefix } = await (0, auth_1.generateApiKey)();
|
|
56
|
+
await (0, db_1.query)(`INSERT INTO api_keys (user_id, key_hash, key_prefix, name)
|
|
57
|
+
VALUES ($1, $2, $3, $4)`, [user.id, hash, prefix, 'Default API Key']);
|
|
58
|
+
// Generate session token
|
|
59
|
+
const sessionToken = (0, auth_1.generateSessionToken)({
|
|
60
|
+
id: user.id,
|
|
61
|
+
email: user.email,
|
|
62
|
+
tier: user.tier,
|
|
63
|
+
});
|
|
64
|
+
res.status(201).json({
|
|
65
|
+
message: 'Account created successfully',
|
|
66
|
+
user: {
|
|
67
|
+
id: user.id,
|
|
68
|
+
email: user.email,
|
|
69
|
+
tier: user.tier,
|
|
70
|
+
},
|
|
71
|
+
apiKey: key,
|
|
72
|
+
sessionToken,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
console.error('[Auth] Signup error:', error);
|
|
77
|
+
res.status(500).json({
|
|
78
|
+
error: {
|
|
79
|
+
code: 'INTERNAL_ERROR',
|
|
80
|
+
message: 'Failed to create account',
|
|
81
|
+
},
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
/**
|
|
86
|
+
* POST /api/auth/login
|
|
87
|
+
* Login and get session token
|
|
88
|
+
*/
|
|
89
|
+
router.post('/login', async (req, res) => {
|
|
90
|
+
try {
|
|
91
|
+
const { email, password } = req.body;
|
|
92
|
+
if (!email || !password) {
|
|
93
|
+
res.status(400).json({
|
|
94
|
+
error: {
|
|
95
|
+
code: 'VALIDATION_ERROR',
|
|
96
|
+
message: 'Email and password are required',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
// Find user
|
|
102
|
+
const user = await (0, db_1.queryOne)('SELECT * FROM users WHERE email = $1', [email.toLowerCase()]);
|
|
103
|
+
if (!user) {
|
|
104
|
+
res.status(401).json({
|
|
105
|
+
error: {
|
|
106
|
+
code: 'INVALID_CREDENTIALS',
|
|
107
|
+
message: 'Invalid email or password',
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
// Verify password
|
|
113
|
+
const valid = await (0, auth_1.verifyPassword)(password, user.password_hash);
|
|
114
|
+
if (!valid) {
|
|
115
|
+
res.status(401).json({
|
|
116
|
+
error: {
|
|
117
|
+
code: 'INVALID_CREDENTIALS',
|
|
118
|
+
message: 'Invalid email or password',
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
// Generate session token
|
|
124
|
+
const sessionToken = (0, auth_1.generateSessionToken)({
|
|
125
|
+
id: user.id,
|
|
126
|
+
email: user.email,
|
|
127
|
+
tier: user.tier,
|
|
128
|
+
});
|
|
129
|
+
res.json({
|
|
130
|
+
message: 'Login successful',
|
|
131
|
+
user: {
|
|
132
|
+
id: user.id,
|
|
133
|
+
email: user.email,
|
|
134
|
+
tier: user.tier,
|
|
135
|
+
},
|
|
136
|
+
sessionToken,
|
|
137
|
+
expiresIn: '7d',
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
console.error('[Auth] Login error:', error);
|
|
142
|
+
res.status(500).json({
|
|
143
|
+
error: {
|
|
144
|
+
code: 'INTERNAL_ERROR',
|
|
145
|
+
message: 'Login failed',
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
/**
|
|
151
|
+
* GET /api/auth/me
|
|
152
|
+
* Get current user info
|
|
153
|
+
*/
|
|
154
|
+
router.get('/me', auth_1.authenticateApiKey, async (req, res) => {
|
|
155
|
+
try {
|
|
156
|
+
const user = await (0, db_1.queryOne)('SELECT id, email, tier, stripe_customer_id, created_at FROM users WHERE id = $1', [req.user.id]);
|
|
157
|
+
if (!user) {
|
|
158
|
+
res.status(404).json({
|
|
159
|
+
error: {
|
|
160
|
+
code: 'NOT_FOUND',
|
|
161
|
+
message: 'User not found',
|
|
162
|
+
},
|
|
163
|
+
});
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
// Get API key count
|
|
167
|
+
const apiKeys = await (0, db_1.query)('SELECT COUNT(*) as count FROM api_keys WHERE user_id = $1 AND revoked_at IS NULL', [user.id]);
|
|
168
|
+
// Get twin count
|
|
169
|
+
const twins = await (0, db_1.query)('SELECT COUNT(*) as count FROM twins WHERE user_id = $1', [user.id]);
|
|
170
|
+
res.json({
|
|
171
|
+
user: {
|
|
172
|
+
id: user.id,
|
|
173
|
+
email: user.email,
|
|
174
|
+
tier: user.tier,
|
|
175
|
+
createdAt: user.created_at,
|
|
176
|
+
},
|
|
177
|
+
stats: {
|
|
178
|
+
apiKeys: parseInt(apiKeys[0]?.count || '0'),
|
|
179
|
+
twins: parseInt(twins[0]?.count || '0'),
|
|
180
|
+
},
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
catch (error) {
|
|
184
|
+
console.error('[Auth] Get me error:', error);
|
|
185
|
+
res.status(500).json({
|
|
186
|
+
error: {
|
|
187
|
+
code: 'INTERNAL_ERROR',
|
|
188
|
+
message: 'Failed to get user info',
|
|
189
|
+
},
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
});
|
|
193
|
+
/**
|
|
194
|
+
* POST /api/auth/api-keys
|
|
195
|
+
* Create a new API key
|
|
196
|
+
*/
|
|
197
|
+
router.post('/api-keys', auth_1.authenticateApiKey, async (req, res) => {
|
|
198
|
+
try {
|
|
199
|
+
const { name } = req.body;
|
|
200
|
+
// Generate new key
|
|
201
|
+
const { key, hash, prefix } = await (0, auth_1.generateApiKey)();
|
|
202
|
+
const keys = await (0, db_1.query)(`INSERT INTO api_keys (user_id, key_hash, key_prefix, name)
|
|
203
|
+
VALUES ($1, $2, $3, $4)
|
|
204
|
+
RETURNING id, key_prefix, name, created_at`, [req.user.id, hash, prefix, name || 'API Key']);
|
|
205
|
+
res.status(201).json({
|
|
206
|
+
message: 'API key created',
|
|
207
|
+
apiKey: {
|
|
208
|
+
id: keys[0].id,
|
|
209
|
+
key, // Only returned once at creation
|
|
210
|
+
prefix: keys[0].key_prefix,
|
|
211
|
+
name: keys[0].name,
|
|
212
|
+
createdAt: keys[0].created_at,
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
console.error('[Auth] Create API key error:', error);
|
|
218
|
+
res.status(500).json({
|
|
219
|
+
error: {
|
|
220
|
+
code: 'INTERNAL_ERROR',
|
|
221
|
+
message: 'Failed to create API key',
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
/**
|
|
227
|
+
* GET /api/auth/api-keys
|
|
228
|
+
* List user's API keys
|
|
229
|
+
*/
|
|
230
|
+
router.get('/api-keys', auth_1.authenticateApiKey, async (req, res) => {
|
|
231
|
+
try {
|
|
232
|
+
const keys = await (0, db_1.query)(`SELECT id, key_prefix, name, last_used_at, created_at
|
|
233
|
+
FROM api_keys
|
|
234
|
+
WHERE user_id = $1 AND revoked_at IS NULL
|
|
235
|
+
ORDER BY created_at DESC`, [req.user.id]);
|
|
236
|
+
res.json({
|
|
237
|
+
apiKeys: keys.map((k) => ({
|
|
238
|
+
id: k.id,
|
|
239
|
+
prefix: k.key_prefix,
|
|
240
|
+
name: k.name,
|
|
241
|
+
lastUsedAt: k.last_used_at,
|
|
242
|
+
createdAt: k.created_at,
|
|
243
|
+
})),
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
catch (error) {
|
|
247
|
+
console.error('[Auth] List API keys error:', error);
|
|
248
|
+
res.status(500).json({
|
|
249
|
+
error: {
|
|
250
|
+
code: 'INTERNAL_ERROR',
|
|
251
|
+
message: 'Failed to list API keys',
|
|
252
|
+
},
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
/**
|
|
257
|
+
* DELETE /api/auth/api-keys/:id
|
|
258
|
+
* Revoke an API key
|
|
259
|
+
*/
|
|
260
|
+
router.delete('/api-keys/:id', auth_1.authenticateApiKey, async (req, res) => {
|
|
261
|
+
try {
|
|
262
|
+
const { id } = req.params;
|
|
263
|
+
const result = await (0, db_1.query)(`UPDATE api_keys
|
|
264
|
+
SET revoked_at = NOW()
|
|
265
|
+
WHERE id = $1 AND user_id = $2 AND revoked_at IS NULL
|
|
266
|
+
RETURNING id`, [id, req.user.id]);
|
|
267
|
+
if (result.length === 0) {
|
|
268
|
+
res.status(404).json({
|
|
269
|
+
error: {
|
|
270
|
+
code: 'NOT_FOUND',
|
|
271
|
+
message: 'API key not found',
|
|
272
|
+
},
|
|
273
|
+
});
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
res.json({
|
|
277
|
+
message: 'API key revoked',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
console.error('[Auth] Revoke API key error:', error);
|
|
282
|
+
res.status(500).json({
|
|
283
|
+
error: {
|
|
284
|
+
code: 'INTERNAL_ERROR',
|
|
285
|
+
message: 'Failed to revoke API key',
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
exports.default = router;
|
|
291
|
+
//# sourceMappingURL=auth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../../../src/cloud/routes/auth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"billing.d.ts","sourceRoot":"","sources":["../../../src/cloud/routes/billing.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAgBH,QAAA,MAAM,MAAM,4CAAW,CAAC;AAwbxB,eAAe,MAAM,CAAC"}
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Billing Routes
|
|
4
|
+
* MCP Twin Cloud
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const express_1 = require("express");
|
|
8
|
+
const db_1 = require("../db");
|
|
9
|
+
const auth_1 = require("../auth");
|
|
10
|
+
const stripe_1 = require("../stripe");
|
|
11
|
+
const router = (0, express_1.Router)();
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/billing/status
|
|
14
|
+
* Get billing status and current plan
|
|
15
|
+
*/
|
|
16
|
+
router.get('/status', auth_1.authenticateApiKey, async (req, res) => {
|
|
17
|
+
try {
|
|
18
|
+
if (!(0, stripe_1.isStripeConfigured)()) {
|
|
19
|
+
res.json({
|
|
20
|
+
configured: false,
|
|
21
|
+
message: 'Stripe is not configured. Set STRIPE_SECRET_KEY to enable billing.',
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const user = await (0, db_1.queryOne)('SELECT id, email, tier, stripe_customer_id, stripe_subscription_id FROM users WHERE id = $1', [req.user.id]);
|
|
26
|
+
if (!user) {
|
|
27
|
+
res.status(404).json({
|
|
28
|
+
error: { code: 'NOT_FOUND', message: 'User not found' },
|
|
29
|
+
});
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
res.json({
|
|
33
|
+
configured: true,
|
|
34
|
+
tier: user.tier,
|
|
35
|
+
hasSubscription: !!user.stripe_subscription_id,
|
|
36
|
+
customerId: user.stripe_customer_id ? `cus_...${user.stripe_customer_id.slice(-4)}` : null,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
console.error('[Billing] Status error:', error);
|
|
41
|
+
res.status(500).json({
|
|
42
|
+
error: { code: 'INTERNAL_ERROR', message: 'Failed to get billing status' },
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
/**
|
|
47
|
+
* GET /api/billing/plans
|
|
48
|
+
* Get available plans
|
|
49
|
+
*/
|
|
50
|
+
router.get('/plans', async (req, res) => {
|
|
51
|
+
res.json({
|
|
52
|
+
plans: [
|
|
53
|
+
{
|
|
54
|
+
id: 'free',
|
|
55
|
+
name: 'Free',
|
|
56
|
+
price: 0,
|
|
57
|
+
interval: null,
|
|
58
|
+
features: [
|
|
59
|
+
'1 twin',
|
|
60
|
+
'10,000 requests/month',
|
|
61
|
+
'24-hour log retention',
|
|
62
|
+
'Community support',
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
id: 'starter',
|
|
67
|
+
name: 'Starter',
|
|
68
|
+
price: 29,
|
|
69
|
+
interval: 'month',
|
|
70
|
+
priceId: stripe_1.TIER_TO_PRICE.starter,
|
|
71
|
+
features: [
|
|
72
|
+
'5 twins',
|
|
73
|
+
'100,000 requests/month',
|
|
74
|
+
'7-day log retention',
|
|
75
|
+
'Email support',
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
id: 'pro',
|
|
80
|
+
name: 'Professional',
|
|
81
|
+
price: 149,
|
|
82
|
+
interval: 'month',
|
|
83
|
+
priceId: stripe_1.TIER_TO_PRICE.pro,
|
|
84
|
+
features: [
|
|
85
|
+
'Unlimited twins',
|
|
86
|
+
'1,000,000 requests/month',
|
|
87
|
+
'30-day log retention',
|
|
88
|
+
'Priority support',
|
|
89
|
+
'Custom integrations',
|
|
90
|
+
],
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
id: 'enterprise',
|
|
94
|
+
name: 'Enterprise',
|
|
95
|
+
price: null,
|
|
96
|
+
interval: null,
|
|
97
|
+
features: [
|
|
98
|
+
'Unlimited everything',
|
|
99
|
+
'90-day log retention',
|
|
100
|
+
'Dedicated support',
|
|
101
|
+
'SLA guarantee',
|
|
102
|
+
'SSO/SAML',
|
|
103
|
+
'Custom contracts',
|
|
104
|
+
],
|
|
105
|
+
contactSales: true,
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
/**
|
|
111
|
+
* POST /api/billing/checkout
|
|
112
|
+
* Create a Stripe checkout session
|
|
113
|
+
*/
|
|
114
|
+
router.post('/checkout', auth_1.authenticateApiKey, async (req, res) => {
|
|
115
|
+
try {
|
|
116
|
+
if (!(0, stripe_1.isStripeConfigured)()) {
|
|
117
|
+
res.status(400).json({
|
|
118
|
+
error: {
|
|
119
|
+
code: 'STRIPE_NOT_CONFIGURED',
|
|
120
|
+
message: 'Stripe is not configured',
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const { plan, successUrl, cancelUrl } = req.body;
|
|
126
|
+
if (!plan || !successUrl || !cancelUrl) {
|
|
127
|
+
res.status(400).json({
|
|
128
|
+
error: {
|
|
129
|
+
code: 'VALIDATION_ERROR',
|
|
130
|
+
message: 'plan, successUrl, and cancelUrl are required',
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
const priceId = stripe_1.TIER_TO_PRICE[plan];
|
|
136
|
+
if (!priceId) {
|
|
137
|
+
res.status(400).json({
|
|
138
|
+
error: {
|
|
139
|
+
code: 'INVALID_PLAN',
|
|
140
|
+
message: `Invalid plan: ${plan}. Valid plans: starter, pro`,
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
return;
|
|
144
|
+
}
|
|
145
|
+
const user = await (0, db_1.queryOne)('SELECT id, email, stripe_customer_id FROM users WHERE id = $1', [req.user.id]);
|
|
146
|
+
if (!user) {
|
|
147
|
+
res.status(404).json({
|
|
148
|
+
error: { code: 'NOT_FOUND', message: 'User not found' },
|
|
149
|
+
});
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
// Get or create Stripe customer
|
|
153
|
+
const customerId = await (0, stripe_1.getOrCreateCustomer)(user.id, user.email, user.stripe_customer_id);
|
|
154
|
+
// Update user with customer ID if new
|
|
155
|
+
if (!user.stripe_customer_id) {
|
|
156
|
+
await (0, db_1.query)('UPDATE users SET stripe_customer_id = $1 WHERE id = $2', [customerId, user.id]);
|
|
157
|
+
}
|
|
158
|
+
// Create checkout session
|
|
159
|
+
const session = await (0, stripe_1.createCheckoutSession)(customerId, priceId, successUrl, cancelUrl);
|
|
160
|
+
res.json({
|
|
161
|
+
sessionId: session.id,
|
|
162
|
+
url: session.url,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
catch (error) {
|
|
166
|
+
console.error('[Billing] Checkout error:', error);
|
|
167
|
+
res.status(500).json({
|
|
168
|
+
error: { code: 'INTERNAL_ERROR', message: 'Failed to create checkout session' },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
/**
|
|
173
|
+
* POST /api/billing/portal
|
|
174
|
+
* Create a Stripe billing portal session
|
|
175
|
+
*/
|
|
176
|
+
router.post('/portal', auth_1.authenticateApiKey, async (req, res) => {
|
|
177
|
+
try {
|
|
178
|
+
if (!(0, stripe_1.isStripeConfigured)()) {
|
|
179
|
+
res.status(400).json({
|
|
180
|
+
error: {
|
|
181
|
+
code: 'STRIPE_NOT_CONFIGURED',
|
|
182
|
+
message: 'Stripe is not configured',
|
|
183
|
+
},
|
|
184
|
+
});
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const { returnUrl } = req.body;
|
|
188
|
+
if (!returnUrl) {
|
|
189
|
+
res.status(400).json({
|
|
190
|
+
error: {
|
|
191
|
+
code: 'VALIDATION_ERROR',
|
|
192
|
+
message: 'returnUrl is required',
|
|
193
|
+
},
|
|
194
|
+
});
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const user = await (0, db_1.queryOne)('SELECT stripe_customer_id FROM users WHERE id = $1', [req.user.id]);
|
|
198
|
+
if (!user?.stripe_customer_id) {
|
|
199
|
+
res.status(400).json({
|
|
200
|
+
error: {
|
|
201
|
+
code: 'NO_SUBSCRIPTION',
|
|
202
|
+
message: 'No billing account found. Subscribe to a plan first.',
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const session = await (0, stripe_1.createPortalSession)(user.stripe_customer_id, returnUrl);
|
|
208
|
+
res.json({
|
|
209
|
+
url: session.url,
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
catch (error) {
|
|
213
|
+
console.error('[Billing] Portal error:', error);
|
|
214
|
+
res.status(500).json({
|
|
215
|
+
error: { code: 'INTERNAL_ERROR', message: 'Failed to create portal session' },
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
/**
|
|
220
|
+
* GET /api/billing/invoices
|
|
221
|
+
* Get user's invoices
|
|
222
|
+
*/
|
|
223
|
+
router.get('/invoices', auth_1.authenticateApiKey, async (req, res) => {
|
|
224
|
+
try {
|
|
225
|
+
if (!(0, stripe_1.isStripeConfigured)()) {
|
|
226
|
+
res.json({ invoices: [] });
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
const user = await (0, db_1.queryOne)('SELECT stripe_customer_id FROM users WHERE id = $1', [req.user.id]);
|
|
230
|
+
if (!user?.stripe_customer_id) {
|
|
231
|
+
res.json({ invoices: [] });
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const invoices = await (0, stripe_1.getInvoices)(user.stripe_customer_id);
|
|
235
|
+
res.json({
|
|
236
|
+
invoices: invoices.map((inv) => ({
|
|
237
|
+
id: inv.id,
|
|
238
|
+
number: inv.number,
|
|
239
|
+
status: inv.status,
|
|
240
|
+
amount: inv.amount_due / 100,
|
|
241
|
+
currency: inv.currency,
|
|
242
|
+
created: new Date(inv.created * 1000).toISOString(),
|
|
243
|
+
pdfUrl: inv.invoice_pdf,
|
|
244
|
+
hostedUrl: inv.hosted_invoice_url,
|
|
245
|
+
})),
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
catch (error) {
|
|
249
|
+
console.error('[Billing] Invoices error:', error);
|
|
250
|
+
res.status(500).json({
|
|
251
|
+
error: { code: 'INTERNAL_ERROR', message: 'Failed to get invoices' },
|
|
252
|
+
});
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
/**
|
|
256
|
+
* POST /api/billing/webhook
|
|
257
|
+
* Handle Stripe webhooks
|
|
258
|
+
*/
|
|
259
|
+
router.post('/webhook',
|
|
260
|
+
// Use raw body for webhook signature verification
|
|
261
|
+
async (req, res) => {
|
|
262
|
+
const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
|
|
263
|
+
if (!webhookSecret) {
|
|
264
|
+
console.error('[Billing] STRIPE_WEBHOOK_SECRET not configured');
|
|
265
|
+
res.status(400).json({ error: 'Webhook not configured' });
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
const signature = req.headers['stripe-signature'];
|
|
269
|
+
if (!signature) {
|
|
270
|
+
res.status(400).json({ error: 'Missing stripe-signature header' });
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
let event;
|
|
274
|
+
try {
|
|
275
|
+
// req.body should be raw buffer for webhook verification
|
|
276
|
+
const rawBody = req.body;
|
|
277
|
+
event = (0, stripe_1.constructWebhookEvent)(rawBody, signature, webhookSecret);
|
|
278
|
+
}
|
|
279
|
+
catch (err) {
|
|
280
|
+
console.error('[Billing] Webhook signature verification failed:', err.message);
|
|
281
|
+
res.status(400).json({ error: `Webhook Error: ${err.message}` });
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
console.log(`[Billing] Webhook received: ${event.type}`);
|
|
285
|
+
try {
|
|
286
|
+
switch (event.type) {
|
|
287
|
+
case 'checkout.session.completed': {
|
|
288
|
+
const session = event.data.object;
|
|
289
|
+
await handleCheckoutComplete(session);
|
|
290
|
+
break;
|
|
291
|
+
}
|
|
292
|
+
case 'customer.subscription.created':
|
|
293
|
+
case 'customer.subscription.updated': {
|
|
294
|
+
const subscription = event.data.object;
|
|
295
|
+
await handleSubscriptionUpdate(subscription);
|
|
296
|
+
break;
|
|
297
|
+
}
|
|
298
|
+
case 'customer.subscription.deleted': {
|
|
299
|
+
const subscription = event.data.object;
|
|
300
|
+
await handleSubscriptionDeleted(subscription);
|
|
301
|
+
break;
|
|
302
|
+
}
|
|
303
|
+
case 'invoice.payment_failed': {
|
|
304
|
+
const invoice = event.data.object;
|
|
305
|
+
await handlePaymentFailed(invoice);
|
|
306
|
+
break;
|
|
307
|
+
}
|
|
308
|
+
default:
|
|
309
|
+
console.log(`[Billing] Unhandled event type: ${event.type}`);
|
|
310
|
+
}
|
|
311
|
+
res.json({ received: true });
|
|
312
|
+
}
|
|
313
|
+
catch (error) {
|
|
314
|
+
console.error('[Billing] Webhook handler error:', error);
|
|
315
|
+
res.status(500).json({ error: 'Webhook handler failed' });
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
/**
|
|
319
|
+
* Handle checkout.session.completed
|
|
320
|
+
*/
|
|
321
|
+
async function handleCheckoutComplete(session) {
|
|
322
|
+
const customerId = session.customer;
|
|
323
|
+
const subscriptionId = session.subscription;
|
|
324
|
+
console.log(`[Billing] Checkout complete for customer ${customerId}`);
|
|
325
|
+
// Update user with subscription ID
|
|
326
|
+
await (0, db_1.query)(`UPDATE users
|
|
327
|
+
SET stripe_subscription_id = $1, updated_at = NOW()
|
|
328
|
+
WHERE stripe_customer_id = $2`, [subscriptionId, customerId]);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* Handle subscription created/updated
|
|
332
|
+
*/
|
|
333
|
+
async function handleSubscriptionUpdate(subscription) {
|
|
334
|
+
const customerId = subscription.customer;
|
|
335
|
+
const status = subscription.status;
|
|
336
|
+
const priceId = subscription.items.data[0]?.price?.id;
|
|
337
|
+
console.log(`[Billing] Subscription update: ${customerId} -> ${status} (${priceId})`);
|
|
338
|
+
if (status === 'active' || status === 'trialing') {
|
|
339
|
+
// Determine tier from price
|
|
340
|
+
const tier = stripe_1.PRICE_TO_TIER[priceId] || 'free';
|
|
341
|
+
await (0, db_1.query)(`UPDATE users
|
|
342
|
+
SET tier = $1, stripe_subscription_id = $2, updated_at = NOW()
|
|
343
|
+
WHERE stripe_customer_id = $3`, [tier, subscription.id, customerId]);
|
|
344
|
+
console.log(`[Billing] User upgraded to ${tier}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
/**
|
|
348
|
+
* Handle subscription deleted
|
|
349
|
+
*/
|
|
350
|
+
async function handleSubscriptionDeleted(subscription) {
|
|
351
|
+
const customerId = subscription.customer;
|
|
352
|
+
console.log(`[Billing] Subscription deleted for ${customerId}`);
|
|
353
|
+
// Downgrade to free tier
|
|
354
|
+
await (0, db_1.query)(`UPDATE users
|
|
355
|
+
SET tier = 'free', stripe_subscription_id = NULL, updated_at = NOW()
|
|
356
|
+
WHERE stripe_customer_id = $1`, [customerId]);
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Handle payment failed
|
|
360
|
+
*/
|
|
361
|
+
async function handlePaymentFailed(invoice) {
|
|
362
|
+
const customerId = invoice.customer;
|
|
363
|
+
console.log(`[Billing] Payment failed for ${customerId}`);
|
|
364
|
+
// Could send email notification, etc.
|
|
365
|
+
// For now just log it
|
|
366
|
+
}
|
|
367
|
+
exports.default = router;
|
|
368
|
+
//# sourceMappingURL=billing.js.map
|