@quiqflow-org/quiqflow-multi-tenants-utils 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/CONTRIBUTING.md +114 -0
- package/LICENSE +21 -0
- package/dist/SchemaResolutionService.d.ts +12 -0
- package/dist/SchemaResolutionService.d.ts.map +1 -0
- package/dist/SchemaResolutionService.js +52 -0
- package/dist/TenantConnectionManager.d.ts +18 -0
- package/dist/TenantConnectionManager.d.ts.map +1 -0
- package/dist/TenantConnectionManager.js +61 -0
- package/dist/authHelpers.d.ts +44 -0
- package/dist/authHelpers.d.ts.map +1 -0
- package/dist/authHelpers.js +106 -0
- package/dist/cache.d.ts +10 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +28 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +23 -0
- package/dist/middleware.d.ts +4 -0
- package/dist/middleware.d.ts.map +1 -0
- package/dist/middleware.js +66 -0
- package/dist/tenantHelpers.d.ts +46 -0
- package/dist/tenantHelpers.d.ts.map +1 -0
- package/dist/tenantHelpers.js +91 -0
- package/dist/types.d.ts +52 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/package.json +49 -0
package/CONTRIBUTING.md
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
---
|
|
2
|
+
noteId: "4752778074a811f081017bbaf3f56380"
|
|
3
|
+
tags: []
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# Contributing to Quiqflow common orm
|
|
8
|
+
|
|
9
|
+
We are thrilled that you'd like to contribute to this project! This document provides guidelines for contributing effectively and collaboratively.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## How to Contribute
|
|
14
|
+
|
|
15
|
+
### 1. Report Issues
|
|
16
|
+
|
|
17
|
+
If you encounter a bug, have a feature request, or need clarification, please [open an issue](https://github.com/[REPO]/issues).
|
|
18
|
+
|
|
19
|
+
**When creating an issue:**
|
|
20
|
+
|
|
21
|
+
- Use the provided issue template.
|
|
22
|
+
- Provide a clear, descriptive title.
|
|
23
|
+
- Include steps to reproduce, if applicable.
|
|
24
|
+
- Add screenshots or logs if necessary.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
### 2. Create Pull Requests (PRs)
|
|
29
|
+
|
|
30
|
+
#### Steps for Submitting a PR
|
|
31
|
+
|
|
32
|
+
1. **Fork the repository** and create a new branch:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
git checkout -b feature/your-feature-name
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
2. **Make your changes** while following the project's coding standards.
|
|
39
|
+
3. **Test your changes** thoroughly.
|
|
40
|
+
4. **Commit your changes**:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git commit -m "feat: Add description of the feature"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Follow the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format.
|
|
47
|
+
5. **Push your branch** to your fork:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
git push origin feature/your-feature-name
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
6. **Open a Pull Request**:
|
|
54
|
+
- Provide a descriptive title and summary.
|
|
55
|
+
- Link any related issues (e.g., `Fixes #123`).
|
|
56
|
+
- Follow the PR template.
|
|
57
|
+
|
|
58
|
+
#### Code Review
|
|
59
|
+
|
|
60
|
+
- PRs will be reviewed by maintainers.
|
|
61
|
+
- Address requested changes promptly.
|
|
62
|
+
- Once approved, your changes will be merged.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Code Guidelines
|
|
67
|
+
|
|
68
|
+
- Follow the coding standards defined in the [STYLE_GUIDE.md](./STYLE_GUIDE.md).
|
|
69
|
+
- Use meaningful variable and function names.
|
|
70
|
+
- Write comments where necessary to explain complex logic.
|
|
71
|
+
- Ensure that your code is tested and linted:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
yarn lint
|
|
75
|
+
yarn test
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Branching Strategy
|
|
81
|
+
|
|
82
|
+
- `main`: Stable production-ready code.
|
|
83
|
+
- `develop`: Latest development code.
|
|
84
|
+
- `feature/*`: Features or enhancements.
|
|
85
|
+
- `bugfix/*`: Bug fixes.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Commit Message Format
|
|
90
|
+
|
|
91
|
+
Use the following structure for commit messages:
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
<type>: <short description>
|
|
95
|
+
|
|
96
|
+
[Optional body explaining what and why.]
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Types:**
|
|
100
|
+
|
|
101
|
+
- `feat`: New features
|
|
102
|
+
- `fix`: Bug fixes
|
|
103
|
+
- `docs`: Documentation updates
|
|
104
|
+
- `style`: Code style improvements
|
|
105
|
+
- `refactor`: Code changes without changing functionality
|
|
106
|
+
- `test`: Adding or updating tests
|
|
107
|
+
|
|
108
|
+
---
|
|
109
|
+
|
|
110
|
+
## Questions
|
|
111
|
+
|
|
112
|
+
If you have any questions, feel free to reach out by [creating an issue](https://github.com/quiqflow/quiqflow-multi-tenants-utils/issues) or contacting [mohammad@quiqflow.com].
|
|
113
|
+
|
|
114
|
+
Thank you for contributing to [Quiqflow]!
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Quiqflow-2.0
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { TenantCache } from "./cache";
|
|
2
|
+
import { AdminConnectionLike } from "./types";
|
|
3
|
+
export declare class SchemaResolutionService {
|
|
4
|
+
private admin;
|
|
5
|
+
private cache;
|
|
6
|
+
private static readonly MAX_RETRIES;
|
|
7
|
+
constructor(adminConnection: AdminConnectionLike, cache?: TenantCache);
|
|
8
|
+
preloadAllTenants(): Promise<void>;
|
|
9
|
+
private isActive;
|
|
10
|
+
resolveSchemaForTenant(tenantId: number): Promise<string>;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=SchemaResolutionService.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SchemaResolutionService.d.ts","sourceRoot":"","sources":["../src/SchemaResolutionService.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,SAAS,CAAC;AACtC,OAAO,EAAE,mBAAmB,EAAgB,MAAM,SAAS,CAAC;AAE5D,qBAAa,uBAAuB;IAClC,OAAO,CAAC,KAAK,CAAsB;IACnC,OAAO,CAAC,KAAK,CAAc;IAC3B,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAK;gBAGtC,eAAe,EAAE,mBAAmB,EACpC,KAAK,GAAE,WAAkC;IAMrC,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IAMxC,OAAO,CAAC,QAAQ;IAMV,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;CAsBhE"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SchemaResolutionService = void 0;
|
|
4
|
+
const cache_1 = require("./cache");
|
|
5
|
+
class SchemaResolutionService {
|
|
6
|
+
constructor(adminConnection, cache = cache_1.TenantCache.instance) {
|
|
7
|
+
this.admin = adminConnection;
|
|
8
|
+
this.cache = cache;
|
|
9
|
+
}
|
|
10
|
+
async preloadAllTenants() {
|
|
11
|
+
const tenants = await this.admin.listAllTenants();
|
|
12
|
+
for (const t of tenants)
|
|
13
|
+
if (t.schemaName)
|
|
14
|
+
this.cache.set(String(t.tenantId), t.schemaName);
|
|
15
|
+
}
|
|
16
|
+
isActive(tenant) {
|
|
17
|
+
if (!tenant)
|
|
18
|
+
return false;
|
|
19
|
+
if (!tenant.status)
|
|
20
|
+
return true;
|
|
21
|
+
return tenant.status.toLowerCase() === "active";
|
|
22
|
+
}
|
|
23
|
+
async resolveSchemaForTenant(tenantId) {
|
|
24
|
+
const cacheKey = String(tenantId);
|
|
25
|
+
const cached = this.cache.get(cacheKey);
|
|
26
|
+
if (cached)
|
|
27
|
+
return cached;
|
|
28
|
+
let attempt = 0;
|
|
29
|
+
let lastError;
|
|
30
|
+
while (attempt <= SchemaResolutionService.MAX_RETRIES) {
|
|
31
|
+
try {
|
|
32
|
+
const record = await this.admin.getTenantById(tenantId);
|
|
33
|
+
if (!record || !record.schemaName)
|
|
34
|
+
throw new Error("Tenant not found");
|
|
35
|
+
if (!this.isActive(record))
|
|
36
|
+
throw new Error("Tenant inactive");
|
|
37
|
+
this.cache.set(cacheKey, record.schemaName);
|
|
38
|
+
return record.schemaName;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
lastError = e;
|
|
42
|
+
attempt++;
|
|
43
|
+
if (attempt > SchemaResolutionService.MAX_RETRIES)
|
|
44
|
+
break;
|
|
45
|
+
await new Promise((r) => setTimeout(r, 50 * attempt));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
throw lastError || new Error("Failed resolving tenant schema");
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
exports.SchemaResolutionService = SchemaResolutionService;
|
|
52
|
+
SchemaResolutionService.MAX_RETRIES = 2;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { TenantConnectionManagerOptions } from "./types";
|
|
2
|
+
export declare class TenantConnectionManager {
|
|
3
|
+
private static _instance;
|
|
4
|
+
private connections;
|
|
5
|
+
private factory;
|
|
6
|
+
private authenticate?;
|
|
7
|
+
private constructor();
|
|
8
|
+
static init(opts: TenantConnectionManagerOptions): TenantConnectionManager;
|
|
9
|
+
static getInstance(): TenantConnectionManager;
|
|
10
|
+
getConnection(schemaName: string): Promise<any>;
|
|
11
|
+
/**
|
|
12
|
+
* Efficiently ensure schema isolation for reused connections
|
|
13
|
+
* Fixes Sequelize model schema caching issues in multi-tenant environment
|
|
14
|
+
*/
|
|
15
|
+
private ensureSchemaIsolation;
|
|
16
|
+
closeAll(): Promise<void>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=TenantConnectionManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TenantConnectionManager.d.ts","sourceRoot":"","sources":["../src/TenantConnectionManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,8BAA8B,EAAE,MAAM,SAAS,CAAC;AAEzD,qBAAa,uBAAuB;IAClC,OAAO,CAAC,MAAM,CAAC,SAAS,CAAwC;IAChE,OAAO,CAAC,WAAW,CAA+B;IAClD,OAAO,CAAC,OAAO,CAA4C;IAC3D,OAAO,CAAC,YAAY,CAAC,CAAiD;IAEtE,OAAO;IAKP,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,8BAA8B,GAAG,uBAAuB;IAK1E,MAAM,CAAC,WAAW,IAAI,uBAAuB;IAMvC,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC;IAgBrD;;;OAGG;YACW,qBAAqB;IAgB7B,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAUhC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TenantConnectionManager = void 0;
|
|
4
|
+
class TenantConnectionManager {
|
|
5
|
+
constructor(opts) {
|
|
6
|
+
this.connections = new Map();
|
|
7
|
+
this.factory = opts.factory;
|
|
8
|
+
this.authenticate = opts.authenticate;
|
|
9
|
+
}
|
|
10
|
+
static init(opts) {
|
|
11
|
+
if (!this._instance)
|
|
12
|
+
this._instance = new TenantConnectionManager(opts);
|
|
13
|
+
return this._instance;
|
|
14
|
+
}
|
|
15
|
+
static getInstance() {
|
|
16
|
+
if (!this._instance)
|
|
17
|
+
throw new Error("TenantConnectionManager not initialized");
|
|
18
|
+
return this._instance;
|
|
19
|
+
}
|
|
20
|
+
async getConnection(schemaName) {
|
|
21
|
+
if (!this.connections.has(schemaName)) {
|
|
22
|
+
const orm = await this.factory({ schemaName });
|
|
23
|
+
if (this.authenticate)
|
|
24
|
+
await this.authenticate(orm);
|
|
25
|
+
this.connections.set(schemaName, orm);
|
|
26
|
+
}
|
|
27
|
+
const orm = this.connections.get(schemaName);
|
|
28
|
+
// CRITICAL: Ensure schema isolation when reusing connections
|
|
29
|
+
// This fixes Sequelize model schema caching issues in multi-tenant environment
|
|
30
|
+
await this.ensureSchemaIsolation(orm, schemaName);
|
|
31
|
+
return orm;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Efficiently ensure schema isolation for reused connections
|
|
35
|
+
* Fixes Sequelize model schema caching issues in multi-tenant environment
|
|
36
|
+
*/
|
|
37
|
+
async ensureSchemaIsolation(orm, expectedSchema) {
|
|
38
|
+
// Set search_path for this connection (lightweight operation)
|
|
39
|
+
await orm.sequelize.query(`SET search_path TO "${expectedSchema}", public`);
|
|
40
|
+
// Force all models to use the correct schema
|
|
41
|
+
// Sequelize caches schema names in model definitions, so we need to update them
|
|
42
|
+
Object.values(orm.sequelize.models).forEach((model) => {
|
|
43
|
+
if (model._schema !== expectedSchema) {
|
|
44
|
+
model._schema = expectedSchema;
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
async closeAll() {
|
|
49
|
+
for (const [, orm] of this.connections) {
|
|
50
|
+
if (orm && typeof orm.close === "function") {
|
|
51
|
+
try {
|
|
52
|
+
await orm.close();
|
|
53
|
+
}
|
|
54
|
+
catch { }
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
this.connections.clear();
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.TenantConnectionManager = TenantConnectionManager;
|
|
61
|
+
TenantConnectionManager._instance = null;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Authentication and authorization middleware helpers
|
|
3
|
+
* @author QuiqFlow Team
|
|
4
|
+
*/
|
|
5
|
+
import { Response } from "express";
|
|
6
|
+
import { TenantContextAugmentedRequest } from "./types";
|
|
7
|
+
/**
|
|
8
|
+
* Ensure user has admin role or send error response
|
|
9
|
+
* @param req Express request with tenant context
|
|
10
|
+
* @param res Express response
|
|
11
|
+
* @returns boolean indicating if validation passed
|
|
12
|
+
*/
|
|
13
|
+
export declare const ensureAdmin: (req: TenantContextAugmentedRequest, res: Response) => boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Ensure user has specific role or send error response
|
|
16
|
+
* @param req Express request with tenant context
|
|
17
|
+
* @param res Express response
|
|
18
|
+
* @param requiredRole Required role string
|
|
19
|
+
* @returns boolean indicating if validation passed
|
|
20
|
+
*/
|
|
21
|
+
export declare const ensureRole: (req: TenantContextAugmentedRequest, res: Response, requiredRole: string) => boolean;
|
|
22
|
+
/**
|
|
23
|
+
* Ensure user has any of the specified roles
|
|
24
|
+
* @param req Express request with tenant context
|
|
25
|
+
* @param res Express response
|
|
26
|
+
* @param allowedRoles Array of allowed role strings
|
|
27
|
+
* @returns boolean indicating if validation passed
|
|
28
|
+
*/
|
|
29
|
+
export declare const ensureAnyRole: (req: TenantContextAugmentedRequest, res: Response, allowedRoles: string[]) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Ensure user is authenticated (has user object)
|
|
32
|
+
* @param req Express request with tenant context
|
|
33
|
+
* @param res Express response
|
|
34
|
+
* @returns boolean indicating if validation passed
|
|
35
|
+
*/
|
|
36
|
+
export declare const ensureAuthenticated: (req: TenantContextAugmentedRequest, res: Response) => boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Ensure tenant context is properly set
|
|
39
|
+
* @param req Express request with tenant context
|
|
40
|
+
* @param res Express response
|
|
41
|
+
* @returns boolean indicating if validation passed
|
|
42
|
+
*/
|
|
43
|
+
export declare const ensureTenantContext: (req: TenantContextAugmentedRequest, res: Response) => boolean;
|
|
44
|
+
//# sourceMappingURL=authHelpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"authHelpers.d.ts","sourceRoot":"","sources":["../src/authHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AACnC,OAAO,EAAE,6BAA6B,EAAE,MAAM,SAAS,CAAC;AAGxD;;;;;GAKG;AACH,eAAO,MAAM,WAAW,GACtB,KAAK,6BAA6B,EAClC,KAAK,QAAQ,KACZ,OAaF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,UAAU,GACrB,KAAK,6BAA6B,EAClC,KAAK,QAAQ,EACb,cAAc,MAAM,KACnB,OAeF,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,aAAa,GACxB,KAAK,6BAA6B,EAClC,KAAK,QAAQ,EACb,cAAc,MAAM,EAAE,KACrB,OAgBF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,KAAK,6BAA6B,EAClC,KAAK,QAAQ,KACZ,OAOF,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,mBAAmB,GAC9B,KAAK,6BAA6B,EAClC,KAAK,QAAQ,KACZ,OAcF,CAAC"}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Authentication and authorization middleware helpers
|
|
4
|
+
* @author QuiqFlow Team
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.ensureTenantContext = exports.ensureAuthenticated = exports.ensureAnyRole = exports.ensureRole = exports.ensureAdmin = void 0;
|
|
8
|
+
const tenantHelpers_1 = require("./tenantHelpers");
|
|
9
|
+
/**
|
|
10
|
+
* Ensure user has admin role or send error response
|
|
11
|
+
* @param req Express request with tenant context
|
|
12
|
+
* @param res Express response
|
|
13
|
+
* @returns boolean indicating if validation passed
|
|
14
|
+
*/
|
|
15
|
+
const ensureAdmin = (req, res) => {
|
|
16
|
+
if (!req.user) {
|
|
17
|
+
res.status(401).json({ message: "Authentication required" });
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
if (!(0, tenantHelpers_1.isAdmin)(req)) {
|
|
21
|
+
res.status(403).json({ message: "Admin access required" });
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
console.log("User is admin:", req.user.role);
|
|
25
|
+
return true;
|
|
26
|
+
};
|
|
27
|
+
exports.ensureAdmin = ensureAdmin;
|
|
28
|
+
/**
|
|
29
|
+
* Ensure user has specific role or send error response
|
|
30
|
+
* @param req Express request with tenant context
|
|
31
|
+
* @param res Express response
|
|
32
|
+
* @param requiredRole Required role string
|
|
33
|
+
* @returns boolean indicating if validation passed
|
|
34
|
+
*/
|
|
35
|
+
const ensureRole = (req, res, requiredRole) => {
|
|
36
|
+
if (!req.user) {
|
|
37
|
+
res.status(401).json({ message: "Authentication required" });
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
if (!(0, tenantHelpers_1.hasRole)(req, requiredRole)) {
|
|
41
|
+
res.status(403).json({
|
|
42
|
+
message: `${requiredRole} access required`,
|
|
43
|
+
userRole: req.user.role,
|
|
44
|
+
});
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
};
|
|
49
|
+
exports.ensureRole = ensureRole;
|
|
50
|
+
/**
|
|
51
|
+
* Ensure user has any of the specified roles
|
|
52
|
+
* @param req Express request with tenant context
|
|
53
|
+
* @param res Express response
|
|
54
|
+
* @param allowedRoles Array of allowed role strings
|
|
55
|
+
* @returns boolean indicating if validation passed
|
|
56
|
+
*/
|
|
57
|
+
const ensureAnyRole = (req, res, allowedRoles) => {
|
|
58
|
+
if (!req.user) {
|
|
59
|
+
res.status(401).json({ message: "Authentication required" });
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
if (!(0, tenantHelpers_1.hasAnyRole)(req, allowedRoles)) {
|
|
63
|
+
res.status(403).json({
|
|
64
|
+
message: `Access denied. Required roles: ${allowedRoles.join(", ")}`,
|
|
65
|
+
userRole: req.user.role,
|
|
66
|
+
allowedRoles,
|
|
67
|
+
});
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
return true;
|
|
71
|
+
};
|
|
72
|
+
exports.ensureAnyRole = ensureAnyRole;
|
|
73
|
+
/**
|
|
74
|
+
* Ensure user is authenticated (has user object)
|
|
75
|
+
* @param req Express request with tenant context
|
|
76
|
+
* @param res Express response
|
|
77
|
+
* @returns boolean indicating if validation passed
|
|
78
|
+
*/
|
|
79
|
+
const ensureAuthenticated = (req, res) => {
|
|
80
|
+
if (!req.user || !req.user.id) {
|
|
81
|
+
res.status(401).json({ message: "Authentication required" });
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
return true;
|
|
85
|
+
};
|
|
86
|
+
exports.ensureAuthenticated = ensureAuthenticated;
|
|
87
|
+
/**
|
|
88
|
+
* Ensure tenant context is properly set
|
|
89
|
+
* @param req Express request with tenant context
|
|
90
|
+
* @param res Express response
|
|
91
|
+
* @returns boolean indicating if validation passed
|
|
92
|
+
*/
|
|
93
|
+
const ensureTenantContext = (req, res) => {
|
|
94
|
+
if (!req.schemaName) {
|
|
95
|
+
res.status(400).json({ message: "Tenant context missing" });
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
if (!req.tenantDb) {
|
|
99
|
+
res
|
|
100
|
+
.status(500)
|
|
101
|
+
.json({ message: "Tenant database connection not available" });
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
return true;
|
|
105
|
+
};
|
|
106
|
+
exports.ensureTenantContext = ensureTenantContext;
|
package/dist/cache.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare class TenantCache {
|
|
2
|
+
private static _instance;
|
|
3
|
+
private cache;
|
|
4
|
+
private constructor();
|
|
5
|
+
static get instance(): TenantCache;
|
|
6
|
+
get(key: string): string | undefined;
|
|
7
|
+
set(key: string, value: string): void;
|
|
8
|
+
keys(): string[];
|
|
9
|
+
}
|
|
10
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../src/cache.ts"],"names":[],"mappings":"AAEA,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAC,SAAS,CAA4B;IACpD,OAAO,CAAC,KAAK,CAAY;IAEzB,OAAO;IAIP,MAAM,KAAK,QAAQ,IAAI,WAAW,CAGjC;IAED,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIpC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,IAAI;IAIrC,IAAI,IAAI,MAAM,EAAE;CAGjB"}
|
package/dist/cache.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TenantCache = void 0;
|
|
7
|
+
const node_cache_1 = __importDefault(require("node-cache"));
|
|
8
|
+
class TenantCache {
|
|
9
|
+
constructor() {
|
|
10
|
+
this.cache = new node_cache_1.default({ stdTTL: 0, useClones: false, checkperiod: 0 });
|
|
11
|
+
}
|
|
12
|
+
static get instance() {
|
|
13
|
+
if (!this._instance)
|
|
14
|
+
this._instance = new TenantCache();
|
|
15
|
+
return this._instance;
|
|
16
|
+
}
|
|
17
|
+
get(key) {
|
|
18
|
+
return this.cache.get(key);
|
|
19
|
+
}
|
|
20
|
+
set(key, value) {
|
|
21
|
+
this.cache.set(key, value);
|
|
22
|
+
}
|
|
23
|
+
keys() {
|
|
24
|
+
return this.cache.keys();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.TenantCache = TenantCache;
|
|
28
|
+
TenantCache._instance = null;
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export * from "./types";
|
|
2
|
+
export * from "./cache";
|
|
3
|
+
export * from "./TenantConnectionManager";
|
|
4
|
+
export * from "./SchemaResolutionService";
|
|
5
|
+
export * from "./middleware";
|
|
6
|
+
export * from "./tenantHelpers";
|
|
7
|
+
export * from "./authHelpers";
|
|
8
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC;AACxB,cAAc,2BAA2B,CAAC;AAC1C,cAAc,2BAA2B,CAAC;AAC1C,cAAc,cAAc,CAAC;AAC7B,cAAc,iBAAiB,CAAC;AAChC,cAAc,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./types"), exports);
|
|
18
|
+
__exportStar(require("./cache"), exports);
|
|
19
|
+
__exportStar(require("./TenantConnectionManager"), exports);
|
|
20
|
+
__exportStar(require("./SchemaResolutionService"), exports);
|
|
21
|
+
__exportStar(require("./middleware"), exports);
|
|
22
|
+
__exportStar(require("./tenantHelpers"), exports);
|
|
23
|
+
__exportStar(require("./authHelpers"), exports);
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { NextFunction, Response } from "express";
|
|
2
|
+
import { CreateTenantMiddlewareOptions, TenantContextAugmentedRequest } from "./types";
|
|
3
|
+
export declare const createTenantMiddleware: (options: CreateTenantMiddlewareOptions) => (req: TenantContextAugmentedRequest, res: Response, next: NextFunction) => Promise<void>;
|
|
4
|
+
//# sourceMappingURL=middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"middleware.d.ts","sourceRoot":"","sources":["../src/middleware.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,OAAO,EACL,6BAA6B,EAC7B,6BAA6B,EAC9B,MAAM,SAAS,CAAC;AAIjB,eAAO,MAAM,sBAAsB,GACjC,SAAS,6BAA6B,MAWpC,KAAK,6BAA6B,EAClC,KAAK,QAAQ,EACb,MAAM,YAAY,KACjB,OAAO,CAAC,IAAI,CA2BhB,CAAC"}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.createTenantMiddleware = void 0;
|
|
7
|
+
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
|
|
8
|
+
const defaultDecode = (token) => jsonwebtoken_1.default.decode(token);
|
|
9
|
+
const createTenantMiddleware = (options) => {
|
|
10
|
+
const { schemaResolutionService, connectionManager, decodeJwt = defaultDecode, headerTenantKey = "x-tenant-id", allowOptional = false, } = options;
|
|
11
|
+
return async (req, res, next) => {
|
|
12
|
+
try {
|
|
13
|
+
const tenantData = extractTenant(req, decodeJwt, headerTenantKey);
|
|
14
|
+
if (!(tenantData === null || tenantData === void 0 ? void 0 : tenantData.tenantId)) {
|
|
15
|
+
if (allowOptional)
|
|
16
|
+
return next();
|
|
17
|
+
res.status(400).json({ error: "Tenant ID is required." });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
const numericTenantId = parseInt(tenantData.tenantId, 10);
|
|
21
|
+
if (isNaN(numericTenantId) || numericTenantId <= 0) {
|
|
22
|
+
res.status(400).json({ error: "Invalid tenant ID format." });
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
const schemaName = await schemaResolutionService.resolveSchemaForTenant(numericTenantId);
|
|
26
|
+
const tenantDb = await connectionManager.getConnection(schemaName);
|
|
27
|
+
req.tenantId = tenantData.tenantId;
|
|
28
|
+
req.schemaName = schemaName;
|
|
29
|
+
if (tenantData.user)
|
|
30
|
+
req.user = tenantData.user;
|
|
31
|
+
req.tenantDb = tenantDb;
|
|
32
|
+
next();
|
|
33
|
+
}
|
|
34
|
+
catch (e) {
|
|
35
|
+
console.error("[createTenantMiddleware] error", e);
|
|
36
|
+
res.status(400).json({ error: "Failed to resolve tenant schema." });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
};
|
|
40
|
+
exports.createTenantMiddleware = createTenantMiddleware;
|
|
41
|
+
const extractTenant = (req, decodeJwt, headerTenantKey) => {
|
|
42
|
+
var _a, _b, _c, _d;
|
|
43
|
+
const authHeader = (_a = req.headers) === null || _a === void 0 ? void 0 : _a.authorization;
|
|
44
|
+
if (authHeader === null || authHeader === void 0 ? void 0 : authHeader.startsWith("Bearer ")) {
|
|
45
|
+
const token = authHeader.substring(7);
|
|
46
|
+
const decoded = decodeJwt(token);
|
|
47
|
+
if (decoded === null || decoded === void 0 ? void 0 : decoded.tenantId) {
|
|
48
|
+
return {
|
|
49
|
+
tenantId: String(decoded.tenantId),
|
|
50
|
+
user: {
|
|
51
|
+
tenantId: decoded.tenantId,
|
|
52
|
+
userId: decoded.userId || decoded.id,
|
|
53
|
+
...decoded,
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const headerTenant = (_b = req.headers) === null || _b === void 0 ? void 0 : _b[headerTenantKey];
|
|
59
|
+
if (headerTenant)
|
|
60
|
+
return { tenantId: String(headerTenant) };
|
|
61
|
+
if ((_c = req.query) === null || _c === void 0 ? void 0 : _c.tenantId)
|
|
62
|
+
return { tenantId: String(req.query.tenantId) };
|
|
63
|
+
if ((_d = req.body) === null || _d === void 0 ? void 0 : _d.tenantId)
|
|
64
|
+
return { tenantId: String(req.body.tenantId) };
|
|
65
|
+
return null;
|
|
66
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Tenant context helper functions for multi-tenant applications
|
|
3
|
+
* @author QuiqFlow Team
|
|
4
|
+
*/
|
|
5
|
+
import { TenantContextAugmentedRequest } from "./types";
|
|
6
|
+
/**
|
|
7
|
+
* Returns resolved schema name from tenant middleware context.
|
|
8
|
+
*/
|
|
9
|
+
export declare const getSchemaName: (req: TenantContextAugmentedRequest) => string;
|
|
10
|
+
/**
|
|
11
|
+
* Extract authenticated user id from request (supports id, sub, userId fields).
|
|
12
|
+
*/
|
|
13
|
+
export declare const getTenantUserId: (req: TenantContextAugmentedRequest) => number;
|
|
14
|
+
/**
|
|
15
|
+
* Get tenant ID from request context
|
|
16
|
+
*/
|
|
17
|
+
export declare const getTenantId: (req: TenantContextAugmentedRequest) => string | number;
|
|
18
|
+
/**
|
|
19
|
+
* Simple no-op validator kept for compatibility with previous validateUserSchema guard.
|
|
20
|
+
* Always returns true now that schema resolution is mandatory via middleware.
|
|
21
|
+
* For response-aware validation, use ensureTenantContext from authHelpers.
|
|
22
|
+
*/
|
|
23
|
+
export declare const validateTenantContext: (_req: TenantContextAugmentedRequest) => boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Simple boolean tenant context check (for compatibility)
|
|
26
|
+
* @param req Express request with tenant context
|
|
27
|
+
* @returns boolean indicating if context is valid
|
|
28
|
+
*/
|
|
29
|
+
export declare const ensureTenantContextBoolean: (req: TenantContextAugmentedRequest) => boolean;
|
|
30
|
+
/**
|
|
31
|
+
* Check if the user has admin role
|
|
32
|
+
*/
|
|
33
|
+
export declare const isAdmin: (req: TenantContextAugmentedRequest) => boolean;
|
|
34
|
+
/**
|
|
35
|
+
* Check if user has specific role
|
|
36
|
+
*/
|
|
37
|
+
export declare const hasRole: (req: TenantContextAugmentedRequest, role: string) => boolean;
|
|
38
|
+
/**
|
|
39
|
+
* Get user roles as array (supports both single role string and roles array)
|
|
40
|
+
*/
|
|
41
|
+
export declare const getUserRoles: (req: TenantContextAugmentedRequest) => string[];
|
|
42
|
+
/**
|
|
43
|
+
* Check if user has any of the specified roles
|
|
44
|
+
*/
|
|
45
|
+
export declare const hasAnyRole: (req: TenantContextAugmentedRequest, roles: string[]) => boolean;
|
|
46
|
+
//# sourceMappingURL=tenantHelpers.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"tenantHelpers.d.ts","sourceRoot":"","sources":["../src/tenantHelpers.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,6BAA6B,EAAE,MAAM,SAAS,CAAC;AAExD;;GAEG;AACH,eAAO,MAAM,aAAa,GAAI,KAAK,6BAA6B,KAAG,MAKlE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,eAAe,GAAI,KAAK,6BAA6B,KAAG,MAUpE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,WAAW,GACtB,KAAK,6BAA6B,KACjC,MAAM,GAAG,MAIX,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,GAChC,MAAM,6BAA6B,KAClC,OAAe,CAAC;AAEnB;;;;GAIG;AACH,eAAO,MAAM,0BAA0B,GACrC,KAAK,6BAA6B,KACjC,OAEF,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,OAAO,GAAI,KAAK,6BAA6B,KAAG,OAClB,CAAC;AAE5C;;GAEG;AACH,eAAO,MAAM,OAAO,GAClB,KAAK,6BAA6B,EAClC,MAAM,MAAM,KACX,OAAiD,CAAC;AAErD;;GAEG;AACH,eAAO,MAAM,YAAY,GAAI,KAAK,6BAA6B,KAAG,MAAM,EAOvE,CAAC;AAEF;;GAEG;AACH,eAAO,MAAM,UAAU,GACrB,KAAK,6BAA6B,EAClC,OAAO,MAAM,EAAE,KACd,OAGF,CAAC"}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* @fileoverview Tenant context helper functions for multi-tenant applications
|
|
4
|
+
* @author QuiqFlow Team
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.hasAnyRole = exports.getUserRoles = exports.hasRole = exports.isAdmin = exports.ensureTenantContextBoolean = exports.validateTenantContext = exports.getTenantId = exports.getTenantUserId = exports.getSchemaName = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* Returns resolved schema name from tenant middleware context.
|
|
10
|
+
*/
|
|
11
|
+
const getSchemaName = (req) => {
|
|
12
|
+
if (!req.schemaName)
|
|
13
|
+
throw new Error("Schema name missing from request context");
|
|
14
|
+
return req.schemaName;
|
|
15
|
+
};
|
|
16
|
+
exports.getSchemaName = getSchemaName;
|
|
17
|
+
/**
|
|
18
|
+
* Extract authenticated user id from request (supports id, sub, userId fields).
|
|
19
|
+
*/
|
|
20
|
+
const getTenantUserId = (req) => {
|
|
21
|
+
var _a, _b, _c;
|
|
22
|
+
const raw = ((_a = req.user) === null || _a === void 0 ? void 0 : _a.id) ||
|
|
23
|
+
((_b = req.user) === null || _b === void 0 ? void 0 : _b.userId) ||
|
|
24
|
+
((_c = req.user) === null || _c === void 0 ? void 0 : _c.sub);
|
|
25
|
+
if (!raw)
|
|
26
|
+
throw new Error("User id missing in request user payload");
|
|
27
|
+
const n = typeof raw === "string" ? parseInt(raw, 10) : raw;
|
|
28
|
+
if (isNaN(n))
|
|
29
|
+
throw new Error("User id is not numeric");
|
|
30
|
+
return n;
|
|
31
|
+
};
|
|
32
|
+
exports.getTenantUserId = getTenantUserId;
|
|
33
|
+
/**
|
|
34
|
+
* Get tenant ID from request context
|
|
35
|
+
*/
|
|
36
|
+
const getTenantId = (req) => {
|
|
37
|
+
var _a;
|
|
38
|
+
if (req.tenantId)
|
|
39
|
+
return req.tenantId;
|
|
40
|
+
if ((_a = req.user) === null || _a === void 0 ? void 0 : _a.tenantId)
|
|
41
|
+
return req.user.tenantId;
|
|
42
|
+
throw new Error("Tenant ID missing from request context");
|
|
43
|
+
};
|
|
44
|
+
exports.getTenantId = getTenantId;
|
|
45
|
+
/**
|
|
46
|
+
* Simple no-op validator kept for compatibility with previous validateUserSchema guard.
|
|
47
|
+
* Always returns true now that schema resolution is mandatory via middleware.
|
|
48
|
+
* For response-aware validation, use ensureTenantContext from authHelpers.
|
|
49
|
+
*/
|
|
50
|
+
const validateTenantContext = (_req) => true;
|
|
51
|
+
exports.validateTenantContext = validateTenantContext;
|
|
52
|
+
/**
|
|
53
|
+
* Simple boolean tenant context check (for compatibility)
|
|
54
|
+
* @param req Express request with tenant context
|
|
55
|
+
* @returns boolean indicating if context is valid
|
|
56
|
+
*/
|
|
57
|
+
const ensureTenantContextBoolean = (req) => {
|
|
58
|
+
return !!(req.schemaName && req.tenantDb);
|
|
59
|
+
};
|
|
60
|
+
exports.ensureTenantContextBoolean = ensureTenantContextBoolean;
|
|
61
|
+
/**
|
|
62
|
+
* Check if the user has admin role
|
|
63
|
+
*/
|
|
64
|
+
const isAdmin = (req) => !!(req.user && req.user.role === "admin");
|
|
65
|
+
exports.isAdmin = isAdmin;
|
|
66
|
+
/**
|
|
67
|
+
* Check if user has specific role
|
|
68
|
+
*/
|
|
69
|
+
const hasRole = (req, role) => !!(req.user && req.user.role === role);
|
|
70
|
+
exports.hasRole = hasRole;
|
|
71
|
+
/**
|
|
72
|
+
* Get user roles as array (supports both single role string and roles array)
|
|
73
|
+
*/
|
|
74
|
+
const getUserRoles = (req) => {
|
|
75
|
+
if (!req.user)
|
|
76
|
+
return [];
|
|
77
|
+
if (Array.isArray(req.user.roles))
|
|
78
|
+
return req.user.roles;
|
|
79
|
+
if (req.user.role)
|
|
80
|
+
return [req.user.role];
|
|
81
|
+
return [];
|
|
82
|
+
};
|
|
83
|
+
exports.getUserRoles = getUserRoles;
|
|
84
|
+
/**
|
|
85
|
+
* Check if user has any of the specified roles
|
|
86
|
+
*/
|
|
87
|
+
const hasAnyRole = (req, roles) => {
|
|
88
|
+
const userRoles = (0, exports.getUserRoles)(req);
|
|
89
|
+
return roles.some((role) => userRoles.includes(role));
|
|
90
|
+
};
|
|
91
|
+
exports.hasAnyRole = hasAnyRole;
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { Request } from "express";
|
|
2
|
+
import { NextFunction, Response } from "express";
|
|
3
|
+
export interface TenantAwareUser {
|
|
4
|
+
id?: number | string;
|
|
5
|
+
userId?: number | string;
|
|
6
|
+
tenantId?: number | string;
|
|
7
|
+
role?: string;
|
|
8
|
+
[key: string]: any;
|
|
9
|
+
}
|
|
10
|
+
export interface TenantContextAugmentedRequest extends Request {
|
|
11
|
+
tenantId?: string | number;
|
|
12
|
+
schemaName?: string;
|
|
13
|
+
user?: TenantAwareUser;
|
|
14
|
+
tenantDb?: any;
|
|
15
|
+
}
|
|
16
|
+
export interface SchemaRecord {
|
|
17
|
+
tenantId: number;
|
|
18
|
+
schemaName: string;
|
|
19
|
+
status?: string;
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
|
22
|
+
export interface SchemaResolutionAdapter {
|
|
23
|
+
listAllTenants(): Promise<SchemaRecord[]>;
|
|
24
|
+
getTenantById(tenantId: number): Promise<SchemaRecord | null>;
|
|
25
|
+
}
|
|
26
|
+
export interface AdminConnectionLike {
|
|
27
|
+
getTenantById(tenantId: number): Promise<SchemaRecord | null>;
|
|
28
|
+
listAllTenants(): Promise<SchemaRecord[]>;
|
|
29
|
+
}
|
|
30
|
+
export interface TenantConnectionFactoryArgs {
|
|
31
|
+
schemaName: string;
|
|
32
|
+
}
|
|
33
|
+
export type TenantOrmFactory = (args: TenantConnectionFactoryArgs) => Promise<any>;
|
|
34
|
+
export interface TenantConnectionManagerOptions {
|
|
35
|
+
factory: TenantOrmFactory;
|
|
36
|
+
authenticate?(orm: any): Promise<void>;
|
|
37
|
+
}
|
|
38
|
+
export interface CreateTenantMiddlewareOptions {
|
|
39
|
+
schemaResolutionService: SchemaResolutionServiceLike;
|
|
40
|
+
connectionManager: TenantConnectionManagerLike;
|
|
41
|
+
decodeJwt?(token: string): any;
|
|
42
|
+
headerTenantKey?: string;
|
|
43
|
+
allowOptional?: boolean;
|
|
44
|
+
}
|
|
45
|
+
export type TenantMiddleware = (req: TenantContextAugmentedRequest, res: Response, next: NextFunction) => Promise<void>;
|
|
46
|
+
export interface TenantConnectionManagerLike {
|
|
47
|
+
getConnection(schemaName: string): Promise<any>;
|
|
48
|
+
}
|
|
49
|
+
export interface SchemaResolutionServiceLike {
|
|
50
|
+
resolveSchemaForTenant(tenantId: number): Promise<string>;
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAClC,OAAO,EAAE,YAAY,EAAE,QAAQ,EAAE,MAAM,SAAS,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACrB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IACzB,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,6BAA8B,SAAQ,OAAO;IAC5D,QAAQ,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;IAC3B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,IAAI,CAAC,EAAE,eAAe,CAAC;IACvB,QAAQ,CAAC,EAAE,GAAG,CAAC;CAChB;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,CAAC,GAAG,EAAE,MAAM,GAAG,GAAG,CAAC;CACpB;AAED,MAAM,WAAW,uBAAuB;IACtC,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAC1C,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAC/D;AAED,MAAM,WAAW,mBAAmB;IAClC,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;IAC9D,cAAc,IAAI,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;CAC3C;AAED,MAAM,WAAW,2BAA2B;IAC1C,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,IAAI,EAAE,2BAA2B,KAC9B,OAAO,CAAC,GAAG,CAAC,CAAC;AAElB,MAAM,WAAW,8BAA8B;IAC7C,OAAO,EAAE,gBAAgB,CAAC;IAC1B,YAAY,CAAC,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,6BAA6B;IAC5C,uBAAuB,EAAE,2BAA2B,CAAC;IACrD,iBAAiB,EAAE,2BAA2B,CAAC;IAC/C,SAAS,CAAC,CAAC,KAAK,EAAE,MAAM,GAAG,GAAG,CAAC;IAC/B,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,MAAM,MAAM,gBAAgB,GAAG,CAC7B,GAAG,EAAE,6BAA6B,EAClC,GAAG,EAAE,QAAQ,EACb,IAAI,EAAE,YAAY,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB,MAAM,WAAW,2BAA2B;IAC1C,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC;CACjD;AAED,MAAM,WAAW,2BAA2B;IAC1C,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC3D"}
|
package/dist/types.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@quiqflow-org/quiqflow-multi-tenants-utils",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Shared multi-tenant helpers (schema resolution, connection management, middleware) for Quiqflow services.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"lint": "eslint 'src/**/*.{ts,js}' || true",
|
|
11
|
+
"prepare": "npm run build",
|
|
12
|
+
"semantic-release": "semantic-release",
|
|
13
|
+
"test": "echo 'No tests yet'"
|
|
14
|
+
},
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+ssh://git@github.com/Quiqflow-2-0/quiqflow-multi-tenants-utils.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"multi-tenant",
|
|
24
|
+
"schema",
|
|
25
|
+
"middleware",
|
|
26
|
+
"quiqflow"
|
|
27
|
+
],
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"jsonwebtoken": "^9.0.2",
|
|
30
|
+
"@semantic-release/github": "^11.0.1",
|
|
31
|
+
"node-cache": "^5.1.2"
|
|
32
|
+
},
|
|
33
|
+
"peerDependencies": {
|
|
34
|
+
"express": ">=4",
|
|
35
|
+
"typescript": ">=5.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@types/express": "^4.17.21",
|
|
39
|
+
"@types/jsonwebtoken": "^9.0.7",
|
|
40
|
+
"@semantic-release/changelog": "^6.0.3",
|
|
41
|
+
"@semantic-release/exec": "^6.0.3",
|
|
42
|
+
"@semantic-release/git": "^10.0.1",
|
|
43
|
+
"@semantic-release/npm": "^11.0.2",
|
|
44
|
+
"semantic-release": "^22.0.12",
|
|
45
|
+
"@types/node": "^22.10.1",
|
|
46
|
+
"express": "^4.21.2",
|
|
47
|
+
"typescript": "^5.3.3"
|
|
48
|
+
}
|
|
49
|
+
}
|