@anton.andrusenko/shopify-mcp-admin 1.1.2 → 2.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 +184 -4
- package/dist/chunk-3Y4P67GZ.js +124 -0
- package/dist/chunk-6YECVENJ.js +208 -0
- package/dist/chunk-GKGHMPEC.js +333 -0
- package/dist/index.js +5391 -2030
- package/dist/mcp-auth-UJ6MADL4.js +15 -0
- package/dist/setup-wizard-PVLOC3DU.js +697 -0
- package/dist/store-MQK3GUUB.js +9 -0
- package/package.json +66 -5
package/README.md
CHANGED
|
@@ -40,9 +40,25 @@ npx @anton.andrusenko/shopify-mcp-admin
|
|
|
40
40
|
npm install -g @anton.andrusenko/shopify-mcp-admin
|
|
41
41
|
```
|
|
42
42
|
|
|
43
|
-
###
|
|
43
|
+
### Setup Wizard (Recommended)
|
|
44
44
|
|
|
45
|
-
|
|
45
|
+
The easiest way to get started is with the interactive setup wizard:
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npx @anton.andrusenko/shopify-mcp-admin init
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
The wizard will:
|
|
52
|
+
1. **Prompt for your store URL** and validate the format
|
|
53
|
+
2. **Choose authentication method** — Legacy token or OAuth 2.0
|
|
54
|
+
3. **Test the connection** — Verifies credentials before saving
|
|
55
|
+
4. **Select your AI client** — Claude Desktop, Cursor, Windsurf, VS Code, LibreChat, or OpenAI
|
|
56
|
+
5. **Configure tool loading** — All tools, role preset, or lazy loading
|
|
57
|
+
6. **Generate and save config** — Creates the appropriate config file for your client
|
|
58
|
+
|
|
59
|
+
### Manual Configuration
|
|
60
|
+
|
|
61
|
+
Alternatively, set environment variables directly:
|
|
46
62
|
|
|
47
63
|
```bash
|
|
48
64
|
export SHOPIFY_STORE_URL=your-store.myshopify.com
|
|
@@ -99,10 +115,13 @@ Tokens are automatically refreshed every 24 hours.
|
|
|
99
115
|
|
|
100
116
|
| Variable | Required | Default | Description |
|
|
101
117
|
|----------|----------|---------|-------------|
|
|
102
|
-
| `
|
|
118
|
+
| `SERVER_MODE` | No | `local` | Server mode: `local` (single-tenant) or `remote` (multi-tenant) |
|
|
119
|
+
| `SHOPIFY_STORE_URL` | ✅ (local mode) | — | Your Shopify store domain (e.g., `your-store.myshopify.com`) |
|
|
103
120
|
| `SHOPIFY_ACCESS_TOKEN` | ⚡ See below | — | Admin API access token from your Custom App |
|
|
104
121
|
| `SHOPIFY_CLIENT_ID` | ⚡ See below | — | Client ID from Dev Dashboard app |
|
|
105
122
|
| `SHOPIFY_CLIENT_SECRET` | ⚡ See below | — | Client Secret from Dev Dashboard app |
|
|
123
|
+
| `DATABASE_URL` | ✅ (remote mode) | — | PostgreSQL connection URL for multi-tenant mode |
|
|
124
|
+
| `ENCRYPTION_KEY` | ✅ (remote mode) | — | 64-char hex key for credential encryption |
|
|
106
125
|
| `SHOPIFY_API_VERSION` | No | `2025-10` | Shopify API version |
|
|
107
126
|
| `TRANSPORT` | No | `stdio` | Transport mode: `stdio` or `http` |
|
|
108
127
|
| `PORT` | No | `3000` | HTTP server port (when `TRANSPORT=http`) |
|
|
@@ -111,7 +130,28 @@ Tokens are automatically refreshed every 24 hours.
|
|
|
111
130
|
| `SHOPIFY_MCP_LAZY_LOADING` | No | `false` | Enable modular lazy loading (set to `true` for on-demand module loading) |
|
|
112
131
|
| `SHOPIFY_MCP_ROLE` | No | — | Role preset for automatic module loading (see [Role Presets](#-role-presets)) |
|
|
113
132
|
|
|
114
|
-
|
|
133
|
+
### Server Modes
|
|
134
|
+
|
|
135
|
+
shopify-mcp-admin supports two operating modes:
|
|
136
|
+
|
|
137
|
+
| Mode | Description | Use Case |
|
|
138
|
+
|------|-------------|----------|
|
|
139
|
+
| **local** (default) | Single-tenant with env credentials | Claude Desktop, local AI agents, development |
|
|
140
|
+
| **remote** | Multi-tenant with database storage | SaaS platform, hosted service |
|
|
141
|
+
|
|
142
|
+
**Local Mode** (default):
|
|
143
|
+
- Credentials via `SHOPIFY_STORE_URL` + `SHOPIFY_ACCESS_TOKEN` (or OAuth)
|
|
144
|
+
- All 79 tools operate on a single store
|
|
145
|
+
- No database required
|
|
146
|
+
- Works with STDIO and HTTP transports
|
|
147
|
+
|
|
148
|
+
**Remote Mode**:
|
|
149
|
+
- Multi-tenant with per-tenant credentials stored in PostgreSQL
|
|
150
|
+
- Requires `DATABASE_URL` and `ENCRYPTION_KEY`
|
|
151
|
+
- HTTP transport only (STDIO not supported for multi-tenant)
|
|
152
|
+
- Web dashboard available at `/app`
|
|
153
|
+
|
|
154
|
+
**⚡ Authentication (local mode):** Provide EITHER `SHOPIFY_ACCESS_TOKEN` (legacy) OR both `SHOPIFY_CLIENT_ID` and `SHOPIFY_CLIENT_SECRET` (Dev Dashboard).
|
|
115
155
|
|
|
116
156
|
### Required Shopify Scopes
|
|
117
157
|
|
|
@@ -1187,6 +1227,94 @@ Enable these settings for the `main` branch in **Settings → Branches**:
|
|
|
1187
1227
|
|
|
1188
1228
|
---
|
|
1189
1229
|
|
|
1230
|
+
## 🚀 Deployment
|
|
1231
|
+
|
|
1232
|
+
shopify-mcp-admin supports multiple deployment options for different environments:
|
|
1233
|
+
|
|
1234
|
+
### Railway (Recommended for Quick Start)
|
|
1235
|
+
|
|
1236
|
+
Deploy to [Railway](https://railway.app) with the included `railway.json` configuration:
|
|
1237
|
+
|
|
1238
|
+
1. Connect your GitHub repository to Railway
|
|
1239
|
+
2. Add environment variables (DATABASE_URL, ENCRYPTION_KEY, etc.)
|
|
1240
|
+
3. Deploy — Railway auto-detects the Dockerfile
|
|
1241
|
+
|
|
1242
|
+
### Kubernetes
|
|
1243
|
+
|
|
1244
|
+
For self-hosted or cloud Kubernetes clusters, see the [Kubernetes Deployment Guide](docs/kubernetes.md).
|
|
1245
|
+
|
|
1246
|
+
Quick start:
|
|
1247
|
+
```bash
|
|
1248
|
+
# Build and push image
|
|
1249
|
+
docker build -t yourregistry/shopify-mcp-admin:v1.0.0 .
|
|
1250
|
+
docker push yourregistry/shopify-mcp-admin:v1.0.0
|
|
1251
|
+
|
|
1252
|
+
# Configure and deploy
|
|
1253
|
+
cd k8s
|
|
1254
|
+
kustomize edit set image shopify-mcp-admin=yourregistry/shopify-mcp-admin:v1.0.0
|
|
1255
|
+
kubectl apply -k .
|
|
1256
|
+
```
|
|
1257
|
+
|
|
1258
|
+
### Docker (Local/Development)
|
|
1259
|
+
|
|
1260
|
+
```bash
|
|
1261
|
+
# Build image
|
|
1262
|
+
docker build -t shopify-mcp-admin .
|
|
1263
|
+
|
|
1264
|
+
# Run container
|
|
1265
|
+
docker run -p 3000:3000 \
|
|
1266
|
+
-e DATABASE_URL="postgresql://..." \
|
|
1267
|
+
-e ENCRYPTION_KEY="..." \
|
|
1268
|
+
shopify-mcp-admin
|
|
1269
|
+
```
|
|
1270
|
+
|
|
1271
|
+
### Image Size
|
|
1272
|
+
|
|
1273
|
+
The production Docker image is optimized for minimal size:
|
|
1274
|
+
- Multi-stage build with Alpine Linux
|
|
1275
|
+
- Production dependencies only (`npm prune --production`)
|
|
1276
|
+
- Target size: <150MB
|
|
1277
|
+
|
|
1278
|
+
---
|
|
1279
|
+
|
|
1280
|
+
## 🎨 Tenant Dashboard (Remote Mode)
|
|
1281
|
+
|
|
1282
|
+
When running in remote mode (`SERVER_MODE=remote`), shopify-mcp-admin includes a web-based dashboard for tenant management. The dashboard provides a modern, responsive UI for:
|
|
1283
|
+
|
|
1284
|
+
- **Shop Management** — View and connect Shopify stores
|
|
1285
|
+
- **API Key Management** — Generate, copy, and revoke API keys
|
|
1286
|
+
- **Usage Statistics** — Monitor API calls and activity
|
|
1287
|
+
- **Account Settings** — Update profile and change password
|
|
1288
|
+
|
|
1289
|
+
### Accessing the Dashboard
|
|
1290
|
+
|
|
1291
|
+
1. Start the server in remote mode:
|
|
1292
|
+
```bash
|
|
1293
|
+
SERVER_MODE=remote npm start
|
|
1294
|
+
```
|
|
1295
|
+
|
|
1296
|
+
2. Navigate to `http://localhost:3000/app` in your browser
|
|
1297
|
+
|
|
1298
|
+
3. Log in with your tenant credentials
|
|
1299
|
+
|
|
1300
|
+
### Dashboard Features
|
|
1301
|
+
|
|
1302
|
+
- ✨ **Modern UI** — Built with React, Vite, and Tailwind CSS
|
|
1303
|
+
- 📱 **Responsive Design** — Works on desktop, tablet, and mobile
|
|
1304
|
+
- ♿ **Accessible** — WCAG AA compliant with keyboard navigation
|
|
1305
|
+
- 🔐 **Secure** — Session-based authentication with HttpOnly cookies
|
|
1306
|
+
- 🎯 **Optimized** — <150KB gzipped bundle size
|
|
1307
|
+
|
|
1308
|
+
### Tech Stack
|
|
1309
|
+
|
|
1310
|
+
- **Frontend:** React 18 + Vite + TypeScript
|
|
1311
|
+
- **Styling:** Tailwind CSS + shadcn/ui components
|
|
1312
|
+
- **State:** React Query for server state management
|
|
1313
|
+
- **Forms:** React Hook Form + Zod validation
|
|
1314
|
+
- **Auth:** Session cookies with automatic expiry handling
|
|
1315
|
+
|
|
1316
|
+
---
|
|
1317
|
+
|
|
1190
1318
|
## 🤝 Contributing
|
|
1191
1319
|
|
|
1192
1320
|
Contributions are welcome! Here's how to get started:
|
|
@@ -1251,3 +1379,55 @@ This project is licensed under the **MIT License** — see the [LICENSE](LICENSE
|
|
|
1251
1379
|
<strong>Made with ❤️ for the AI-powered commerce era</strong>
|
|
1252
1380
|
</p>
|
|
1253
1381
|
|
|
1382
|
+
---
|
|
1383
|
+
|
|
1384
|
+
## 🚀 Deployment
|
|
1385
|
+
|
|
1386
|
+
### Production Deployment
|
|
1387
|
+
|
|
1388
|
+
For production deployment on Railway with Cloudflare, Sentry, and uptime monitoring, see the comprehensive deployment guide:
|
|
1389
|
+
|
|
1390
|
+
📖 **[Production Deployment Guide](./docs/deployment.md)**
|
|
1391
|
+
|
|
1392
|
+
The guide includes:
|
|
1393
|
+
- **Railway Setup**: Step-by-step instructions for deploying to Railway
|
|
1394
|
+
- **Cloudflare DNS**: DNS configuration and SSL/TLS setup
|
|
1395
|
+
- **Database Setup**: Prisma migrations and backup strategy
|
|
1396
|
+
- **Staging Environment**: Separate staging deployment configuration
|
|
1397
|
+
- **Monitoring**: Sentry error tracking and uptime monitoring setup
|
|
1398
|
+
|
|
1399
|
+
### Environment Variables
|
|
1400
|
+
|
|
1401
|
+
For production environment variables, see:
|
|
1402
|
+
|
|
1403
|
+
📄 **[`.env.production.example`](./.env.production.example)**
|
|
1404
|
+
|
|
1405
|
+
This template includes all required and optional environment variables with descriptions, example values, and generation instructions.
|
|
1406
|
+
|
|
1407
|
+
### Quick Start: Local vs Production
|
|
1408
|
+
|
|
1409
|
+
| Mode | Use Case | Transport | Database | Credentials |
|
|
1410
|
+
|------|----------|-----------|----------|-------------|
|
|
1411
|
+
| **Local** | Development, Claude Desktop | STDIO or HTTP | None | Environment variables |
|
|
1412
|
+
| **Remote** | Production, SaaS platform | HTTP only | PostgreSQL | Database (encrypted) |
|
|
1413
|
+
|
|
1414
|
+
**Local Mode** (default):
|
|
1415
|
+
```bash
|
|
1416
|
+
export SHOPIFY_STORE_URL=your-store.myshopify.com
|
|
1417
|
+
export SHOPIFY_ACCESS_TOKEN=shpat_xxxxx
|
|
1418
|
+
npx @anton.andrusenko/shopify-mcp-admin
|
|
1419
|
+
```
|
|
1420
|
+
|
|
1421
|
+
**Remote Mode** (production):
|
|
1422
|
+
```bash
|
|
1423
|
+
export SERVER_MODE=remote
|
|
1424
|
+
export DATABASE_URL=postgresql://...
|
|
1425
|
+
export ENCRYPTION_KEY=<64-char-hex>
|
|
1426
|
+
export TRANSPORT=http
|
|
1427
|
+
# See .env.production.example for all variables
|
|
1428
|
+
```
|
|
1429
|
+
|
|
1430
|
+
---
|
|
1431
|
+
|
|
1432
|
+
</p>
|
|
1433
|
+
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-GKGHMPEC.js";
|
|
4
|
+
|
|
5
|
+
// src/middleware/mcp-auth.ts
|
|
6
|
+
var MCP_AUTH_ERROR_CODES = {
|
|
7
|
+
AUTH_REQUIRED: -32001,
|
|
8
|
+
// Missing authentication
|
|
9
|
+
AUTH_INVALID: -32002,
|
|
10
|
+
// Invalid API key format or key
|
|
11
|
+
AUTH_REVOKED: -32003,
|
|
12
|
+
// API key has been revoked
|
|
13
|
+
AUTH_FORBIDDEN: -32004
|
|
14
|
+
// Tenant suspended or no access
|
|
15
|
+
};
|
|
16
|
+
function createJsonRpcError(code, message, hint) {
|
|
17
|
+
const error = {
|
|
18
|
+
jsonrpc: "2.0",
|
|
19
|
+
error: {
|
|
20
|
+
code,
|
|
21
|
+
message
|
|
22
|
+
},
|
|
23
|
+
id: null
|
|
24
|
+
};
|
|
25
|
+
if (hint) {
|
|
26
|
+
error.error.data = { hint };
|
|
27
|
+
}
|
|
28
|
+
return error;
|
|
29
|
+
}
|
|
30
|
+
function parseBearerToken(authHeader) {
|
|
31
|
+
if (!authHeader) {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
const parts = authHeader.split(" ");
|
|
35
|
+
if (parts.length !== 2 || parts[0].toLowerCase() !== "bearer") {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const token = parts[1];
|
|
39
|
+
if (!token || token.trim() === "") {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
return token;
|
|
43
|
+
}
|
|
44
|
+
async function validateMcpApiKey(authHeader, apiKeyService, prisma) {
|
|
45
|
+
const token = parseBearerToken(authHeader);
|
|
46
|
+
if (!token) {
|
|
47
|
+
return {
|
|
48
|
+
valid: false,
|
|
49
|
+
error: "Authentication required",
|
|
50
|
+
httpStatus: 401
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
const validationResult = await apiKeyService.validate(token);
|
|
54
|
+
if (!validationResult.valid) {
|
|
55
|
+
const isRevoked = validationResult.error?.includes("revoked");
|
|
56
|
+
return {
|
|
57
|
+
valid: false,
|
|
58
|
+
error: validationResult.error || "Invalid API key",
|
|
59
|
+
httpStatus: isRevoked ? 403 : 401
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
const tenant = validationResult.tenant;
|
|
63
|
+
const tenantShops = await prisma.tenantShop.findMany({
|
|
64
|
+
where: {
|
|
65
|
+
tenantId: tenant.tenantId,
|
|
66
|
+
uninstalledAt: null
|
|
67
|
+
// Only active shops
|
|
68
|
+
},
|
|
69
|
+
select: {
|
|
70
|
+
shopDomain: true,
|
|
71
|
+
scopes: true
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
const allowedShops = tenantShops.map((shop) => shop.shopDomain);
|
|
75
|
+
const mcpTenantContext = {
|
|
76
|
+
tenantId: tenant.tenantId,
|
|
77
|
+
email: tenant.email,
|
|
78
|
+
defaultShop: validationResult.shop,
|
|
79
|
+
allowedShops
|
|
80
|
+
};
|
|
81
|
+
return {
|
|
82
|
+
valid: true,
|
|
83
|
+
tenant: mcpTenantContext
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function createMcpAuthMiddleware(options) {
|
|
87
|
+
const { apiKeyService, prisma, isRemote } = options;
|
|
88
|
+
return async (req, res, next) => {
|
|
89
|
+
if (!isRemote) {
|
|
90
|
+
log.debug("[mcp-auth] Local mode: skipping API key validation");
|
|
91
|
+
next();
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
const authResult = await validateMcpApiKey(req.headers.authorization, apiKeyService, prisma);
|
|
95
|
+
if (!authResult.valid) {
|
|
96
|
+
log.debug(`[mcp-auth] Auth failed: ${authResult.error}`);
|
|
97
|
+
let errorCode;
|
|
98
|
+
let hint;
|
|
99
|
+
if (authResult.httpStatus === 401) {
|
|
100
|
+
errorCode = authResult.error === "Authentication required" ? MCP_AUTH_ERROR_CODES.AUTH_REQUIRED : MCP_AUTH_ERROR_CODES.AUTH_INVALID;
|
|
101
|
+
hint = "Include Authorization: Bearer sk_live_xxx header";
|
|
102
|
+
} else {
|
|
103
|
+
errorCode = authResult.error?.includes("revoked") ? MCP_AUTH_ERROR_CODES.AUTH_REVOKED : MCP_AUTH_ERROR_CODES.AUTH_FORBIDDEN;
|
|
104
|
+
hint = "API key has been revoked or tenant is inactive";
|
|
105
|
+
}
|
|
106
|
+
res.status(authResult.httpStatus || 401).json(createJsonRpcError(errorCode, authResult.error || "Authentication failed", hint));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
req.mcpTenantContext = authResult.tenant;
|
|
110
|
+
log.debug(
|
|
111
|
+
`[mcp-auth] Auth successful for tenant: ${authResult.tenant?.tenantId?.substring(0, 8)}...`
|
|
112
|
+
);
|
|
113
|
+
next();
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
var MCP_AUTH_ERRORS = MCP_AUTH_ERROR_CODES;
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
createJsonRpcError,
|
|
120
|
+
parseBearerToken,
|
|
121
|
+
validateMcpApiKey,
|
|
122
|
+
createMcpAuthMiddleware,
|
|
123
|
+
MCP_AUTH_ERRORS
|
|
124
|
+
};
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
import {
|
|
2
|
+
log
|
|
3
|
+
} from "./chunk-GKGHMPEC.js";
|
|
4
|
+
|
|
5
|
+
// src/db/client.ts
|
|
6
|
+
import { PrismaClient } from "@prisma/client";
|
|
7
|
+
import { PrismaClient as PrismaClient2 } from "@prisma/client";
|
|
8
|
+
var prismaInstance = null;
|
|
9
|
+
function getPrismaClient() {
|
|
10
|
+
if (!prismaInstance) {
|
|
11
|
+
log.debug("Creating new Prisma client instance");
|
|
12
|
+
prismaInstance = new PrismaClient({
|
|
13
|
+
log: process.env.DEBUG === "true" ? ["query", "info", "warn", "error"] : ["error"]
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
return prismaInstance;
|
|
17
|
+
}
|
|
18
|
+
async function disconnectPrisma() {
|
|
19
|
+
if (prismaInstance) {
|
|
20
|
+
log.debug("Disconnecting Prisma client");
|
|
21
|
+
await prismaInstance.$disconnect();
|
|
22
|
+
prismaInstance = null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
var prisma = getPrismaClient();
|
|
26
|
+
|
|
27
|
+
// src/session/store.ts
|
|
28
|
+
var SessionStore = class {
|
|
29
|
+
cleanupInterval = null;
|
|
30
|
+
prisma = getPrismaClient();
|
|
31
|
+
SESSION_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
32
|
+
// 24 hours
|
|
33
|
+
/**
|
|
34
|
+
* Retrieve session data by session ID
|
|
35
|
+
* @param sessionId - Unique session identifier
|
|
36
|
+
* @returns SessionData if valid and not expired, null otherwise
|
|
37
|
+
*/
|
|
38
|
+
async get(sessionId) {
|
|
39
|
+
try {
|
|
40
|
+
const session = await this.prisma.tenantSession.findUnique({
|
|
41
|
+
where: { id: sessionId },
|
|
42
|
+
select: {
|
|
43
|
+
tenantId: true,
|
|
44
|
+
expiresAt: true
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
if (!session) {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const expiresAt = session.expiresAt.getTime();
|
|
51
|
+
if (expiresAt < Date.now()) {
|
|
52
|
+
await this.prisma.tenantSession.delete({
|
|
53
|
+
where: { id: sessionId }
|
|
54
|
+
}).catch(() => {
|
|
55
|
+
});
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
tenantId: session.tenantId,
|
|
60
|
+
expiresAt
|
|
61
|
+
};
|
|
62
|
+
} catch (error) {
|
|
63
|
+
log.error("Failed to retrieve session", error instanceof Error ? error : void 0);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Store session data with automatic 24-hour expiration
|
|
69
|
+
* @param sessionId - Unique session identifier
|
|
70
|
+
* @param data - Session data (tenantId)
|
|
71
|
+
*/
|
|
72
|
+
async set(sessionId, data) {
|
|
73
|
+
const expiresAt = new Date(Date.now() + this.SESSION_TTL_MS);
|
|
74
|
+
try {
|
|
75
|
+
await this.prisma.tenantSession.upsert({
|
|
76
|
+
where: { id: sessionId },
|
|
77
|
+
update: {
|
|
78
|
+
expiresAt
|
|
79
|
+
},
|
|
80
|
+
create: {
|
|
81
|
+
id: sessionId,
|
|
82
|
+
tenantId: data.tenantId,
|
|
83
|
+
expiresAt
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
} catch (error) {
|
|
87
|
+
log.error("Failed to store session", error instanceof Error ? error : void 0);
|
|
88
|
+
throw error;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Delete a specific session
|
|
93
|
+
* @param sessionId - Session identifier to delete
|
|
94
|
+
*/
|
|
95
|
+
async delete(sessionId) {
|
|
96
|
+
try {
|
|
97
|
+
await this.prisma.tenantSession.delete({
|
|
98
|
+
where: { id: sessionId }
|
|
99
|
+
});
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error && typeof error === "object" && "code" in error && error.code !== "P2025") {
|
|
102
|
+
log.error("Failed to delete session", error instanceof Error ? error : void 0);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Delete all sessions for a specific tenant
|
|
108
|
+
* Used when a tenant changes their password for security
|
|
109
|
+
* @param tenantId - Tenant identifier
|
|
110
|
+
*/
|
|
111
|
+
async deleteByTenantId(tenantId) {
|
|
112
|
+
try {
|
|
113
|
+
await this.prisma.tenantSession.deleteMany({
|
|
114
|
+
where: { tenantId }
|
|
115
|
+
});
|
|
116
|
+
log.debug(`Deleted all sessions for tenant ${tenantId}`);
|
|
117
|
+
} catch (error) {
|
|
118
|
+
log.error("Failed to delete tenant sessions", error instanceof Error ? error : void 0);
|
|
119
|
+
throw error;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* Remove all expired sessions from the store
|
|
124
|
+
*/
|
|
125
|
+
async cleanup() {
|
|
126
|
+
try {
|
|
127
|
+
const result = await this.prisma.tenantSession.deleteMany({
|
|
128
|
+
where: {
|
|
129
|
+
expiresAt: {
|
|
130
|
+
lt: /* @__PURE__ */ new Date()
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
if (result.count > 0) {
|
|
135
|
+
log.debug(`Cleaned up ${result.count} expired sessions`);
|
|
136
|
+
}
|
|
137
|
+
} catch (error) {
|
|
138
|
+
log.error("Session cleanup failed", error instanceof Error ? error : void 0);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Start automatic cleanup task (runs every 10 minutes)
|
|
143
|
+
* @returns Timer handle for cleanup task
|
|
144
|
+
*/
|
|
145
|
+
startCleanup() {
|
|
146
|
+
if (this.cleanupInterval) {
|
|
147
|
+
return this.cleanupInterval;
|
|
148
|
+
}
|
|
149
|
+
this.cleanupInterval = setInterval(
|
|
150
|
+
() => {
|
|
151
|
+
this.cleanup().catch((error) => {
|
|
152
|
+
log.error("Background session cleanup error", error instanceof Error ? error : void 0);
|
|
153
|
+
});
|
|
154
|
+
},
|
|
155
|
+
10 * 60 * 1e3
|
|
156
|
+
);
|
|
157
|
+
this.cleanupInterval.unref();
|
|
158
|
+
log.info("Session cleanup background task started (runs every 10 minutes)");
|
|
159
|
+
return this.cleanupInterval;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Stop the automatic cleanup task
|
|
163
|
+
*/
|
|
164
|
+
stopCleanup() {
|
|
165
|
+
if (this.cleanupInterval) {
|
|
166
|
+
clearInterval(this.cleanupInterval);
|
|
167
|
+
this.cleanupInterval = null;
|
|
168
|
+
log.info("Session cleanup background task stopped");
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Get the number of active sessions (for testing/monitoring)
|
|
173
|
+
*/
|
|
174
|
+
async size() {
|
|
175
|
+
try {
|
|
176
|
+
return await this.prisma.tenantSession.count({
|
|
177
|
+
where: {
|
|
178
|
+
expiresAt: {
|
|
179
|
+
gte: /* @__PURE__ */ new Date()
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
} catch (error) {
|
|
184
|
+
log.error("Failed to count sessions", error instanceof Error ? error : void 0);
|
|
185
|
+
return 0;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
/**
|
|
189
|
+
* Clear all sessions (for testing)
|
|
190
|
+
*/
|
|
191
|
+
async clear() {
|
|
192
|
+
try {
|
|
193
|
+
await this.prisma.tenantSession.deleteMany();
|
|
194
|
+
} catch (error) {
|
|
195
|
+
log.error("Failed to clear sessions", error instanceof Error ? error : void 0);
|
|
196
|
+
throw error;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
var sessionStore = new SessionStore();
|
|
201
|
+
|
|
202
|
+
export {
|
|
203
|
+
getPrismaClient,
|
|
204
|
+
disconnectPrisma,
|
|
205
|
+
prisma,
|
|
206
|
+
SessionStore,
|
|
207
|
+
sessionStore
|
|
208
|
+
};
|