@meridianjs/auth 0.1.2 → 0.1.4
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/dist/index.d.mts +20 -5
- package/dist/index.d.ts +20 -5
- package/dist/index.js +72 -23
- package/dist/index.mjs +71 -23
- package/package.json +1 -1
package/dist/index.d.mts
CHANGED
|
@@ -27,6 +27,8 @@ interface JwtPayload {
|
|
|
27
27
|
sub: string;
|
|
28
28
|
workspaceId: string | null;
|
|
29
29
|
roles: string[];
|
|
30
|
+
permissions: string[];
|
|
31
|
+
jti?: string;
|
|
30
32
|
iat?: number;
|
|
31
33
|
exp?: number;
|
|
32
34
|
}
|
|
@@ -40,17 +42,17 @@ declare class AuthModuleService extends AuthModuleService_base {
|
|
|
40
42
|
login(input: LoginInput): Promise<AuthResult>;
|
|
41
43
|
/** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
|
|
42
44
|
verifyToken(token: string, secret: string): JwtPayload;
|
|
45
|
+
/** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
|
|
46
|
+
private resolvePermissions;
|
|
43
47
|
private signToken;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Express middleware that validates a Bearer JWT on every request.
|
|
48
52
|
*
|
|
49
|
-
* On success, populates req.user = { id, workspaceId, roles } and calls next().
|
|
53
|
+
* On success, populates req.user = { id, workspaceId, roles, permissions, jti } and calls next().
|
|
54
|
+
* Also validates the session against the DB (stateful revocation support).
|
|
50
55
|
* On failure, responds 401 Unauthorized.
|
|
51
|
-
*
|
|
52
|
-
* Reads jwtSecret from req.scope (the request-scoped DI container that is
|
|
53
|
-
* attached by the framework before any middleware runs).
|
|
54
56
|
*/
|
|
55
57
|
declare function authenticateJWT(req: any, res: Response, next: NextFunction): void;
|
|
56
58
|
|
|
@@ -64,6 +66,19 @@ declare function authenticateJWT(req: any, res: Response, next: NextFunction): v
|
|
|
64
66
|
* { matcher: "/admin/settings", middlewares: [authenticateJWT, requireRoles("admin")] }
|
|
65
67
|
*/
|
|
66
68
|
declare function requireRoles(...roles: string[]): (req: any, res: Response, next: NextFunction) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Permission guard — allows the request if `req.user.roles` includes "super-admin"
|
|
71
|
+
* (full bypass) or if `req.user.permissions` contains at least one of the listed
|
|
72
|
+
* permissions.
|
|
73
|
+
*
|
|
74
|
+
* Must be used after `authenticateJWT` so that `req.user` is populated.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* export const POST = async (req, res) => {
|
|
78
|
+
* requirePermission("project:create")(req, res, async () => { ... })
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
declare function requirePermission(...permissions: string[]): (req: any, res: Response, next: NextFunction) => void;
|
|
67
82
|
/**
|
|
68
83
|
* Workspace isolation guard — rejects requests where the `workspace_id` query
|
|
69
84
|
* param or body field does not match the authenticated user's workspace.
|
|
@@ -78,4 +93,4 @@ declare function requireWorkspace(req: any, res: Response, next: NextFunction):
|
|
|
78
93
|
declare const AUTH_MODULE = "authModuleService";
|
|
79
94
|
declare const _default: _meridianjs_types.ModuleDefinition;
|
|
80
95
|
|
|
81
|
-
export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requireRoles, requireWorkspace };
|
|
96
|
+
export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requirePermission, requireRoles, requireWorkspace };
|
package/dist/index.d.ts
CHANGED
|
@@ -27,6 +27,8 @@ interface JwtPayload {
|
|
|
27
27
|
sub: string;
|
|
28
28
|
workspaceId: string | null;
|
|
29
29
|
roles: string[];
|
|
30
|
+
permissions: string[];
|
|
31
|
+
jti?: string;
|
|
30
32
|
iat?: number;
|
|
31
33
|
exp?: number;
|
|
32
34
|
}
|
|
@@ -40,17 +42,17 @@ declare class AuthModuleService extends AuthModuleService_base {
|
|
|
40
42
|
login(input: LoginInput): Promise<AuthResult>;
|
|
41
43
|
/** Verify a JWT and return its decoded payload. Throws if invalid or expired. */
|
|
42
44
|
verifyToken(token: string, secret: string): JwtPayload;
|
|
45
|
+
/** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
|
|
46
|
+
private resolvePermissions;
|
|
43
47
|
private signToken;
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
/**
|
|
47
51
|
* Express middleware that validates a Bearer JWT on every request.
|
|
48
52
|
*
|
|
49
|
-
* On success, populates req.user = { id, workspaceId, roles } and calls next().
|
|
53
|
+
* On success, populates req.user = { id, workspaceId, roles, permissions, jti } and calls next().
|
|
54
|
+
* Also validates the session against the DB (stateful revocation support).
|
|
50
55
|
* On failure, responds 401 Unauthorized.
|
|
51
|
-
*
|
|
52
|
-
* Reads jwtSecret from req.scope (the request-scoped DI container that is
|
|
53
|
-
* attached by the framework before any middleware runs).
|
|
54
56
|
*/
|
|
55
57
|
declare function authenticateJWT(req: any, res: Response, next: NextFunction): void;
|
|
56
58
|
|
|
@@ -64,6 +66,19 @@ declare function authenticateJWT(req: any, res: Response, next: NextFunction): v
|
|
|
64
66
|
* { matcher: "/admin/settings", middlewares: [authenticateJWT, requireRoles("admin")] }
|
|
65
67
|
*/
|
|
66
68
|
declare function requireRoles(...roles: string[]): (req: any, res: Response, next: NextFunction) => void;
|
|
69
|
+
/**
|
|
70
|
+
* Permission guard — allows the request if `req.user.roles` includes "super-admin"
|
|
71
|
+
* (full bypass) or if `req.user.permissions` contains at least one of the listed
|
|
72
|
+
* permissions.
|
|
73
|
+
*
|
|
74
|
+
* Must be used after `authenticateJWT` so that `req.user` is populated.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* export const POST = async (req, res) => {
|
|
78
|
+
* requirePermission("project:create")(req, res, async () => { ... })
|
|
79
|
+
* }
|
|
80
|
+
*/
|
|
81
|
+
declare function requirePermission(...permissions: string[]): (req: any, res: Response, next: NextFunction) => void;
|
|
67
82
|
/**
|
|
68
83
|
* Workspace isolation guard — rejects requests where the `workspace_id` query
|
|
69
84
|
* param or body field does not match the authenticated user's workspace.
|
|
@@ -78,4 +93,4 @@ declare function requireWorkspace(req: any, res: Response, next: NextFunction):
|
|
|
78
93
|
declare const AUTH_MODULE = "authModuleService";
|
|
79
94
|
declare const _default: _meridianjs_types.ModuleDefinition;
|
|
80
95
|
|
|
81
|
-
export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requireRoles, requireWorkspace };
|
|
96
|
+
export { AUTH_MODULE, AuthModuleService, type AuthResult, type JwtPayload, type LoginInput, type RegisterInput, authenticateJWT, _default as default, requirePermission, requireRoles, requireWorkspace };
|
package/dist/index.js
CHANGED
|
@@ -34,6 +34,7 @@ __export(index_exports, {
|
|
|
34
34
|
AuthModuleService: () => AuthModuleService,
|
|
35
35
|
authenticateJWT: () => authenticateJWT,
|
|
36
36
|
default: () => index_default,
|
|
37
|
+
requirePermission: () => requirePermission,
|
|
37
38
|
requireRoles: () => requireRoles,
|
|
38
39
|
requireWorkspace: () => requireWorkspace
|
|
39
40
|
});
|
|
@@ -44,8 +45,10 @@ var import_framework_utils2 = require("@meridianjs/framework-utils");
|
|
|
44
45
|
var import_framework_utils = require("@meridianjs/framework-utils");
|
|
45
46
|
var import_bcrypt = __toESM(require("bcrypt"));
|
|
46
47
|
var import_jsonwebtoken = __toESM(require("jsonwebtoken"));
|
|
48
|
+
var import_crypto = require("crypto");
|
|
47
49
|
var BCRYPT_ROUNDS = 12;
|
|
48
50
|
var JWT_EXPIRES_IN = "7d";
|
|
51
|
+
var JWT_EXPIRES_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
49
52
|
var AuthModuleService = class extends (0, import_framework_utils.MeridianService)({}) {
|
|
50
53
|
container;
|
|
51
54
|
constructor(container) {
|
|
@@ -74,7 +77,10 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
|
|
|
74
77
|
role,
|
|
75
78
|
is_active: true
|
|
76
79
|
});
|
|
77
|
-
const
|
|
80
|
+
const permissions = await this.resolvePermissions(user.app_role_id);
|
|
81
|
+
const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role], permissions, config.projectConfig.jwtSecret);
|
|
82
|
+
await userService.createSession(jti, user.id, expiresAt).catch(() => {
|
|
83
|
+
});
|
|
78
84
|
return {
|
|
79
85
|
user: {
|
|
80
86
|
id: user.id,
|
|
@@ -102,7 +108,10 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
|
|
|
102
108
|
}
|
|
103
109
|
await userService.recordLogin(user.id).catch(() => {
|
|
104
110
|
});
|
|
105
|
-
const
|
|
111
|
+
const permissions = await this.resolvePermissions(user.app_role_id);
|
|
112
|
+
const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role ?? "member"], permissions, config.projectConfig.jwtSecret);
|
|
113
|
+
await userService.createSession(jti, user.id, expiresAt).catch(() => {
|
|
114
|
+
});
|
|
106
115
|
return {
|
|
107
116
|
user: {
|
|
108
117
|
id: user.id,
|
|
@@ -117,10 +126,23 @@ var AuthModuleService = class extends (0, import_framework_utils.MeridianService
|
|
|
117
126
|
verifyToken(token, secret) {
|
|
118
127
|
return import_jsonwebtoken.default.verify(token, secret);
|
|
119
128
|
}
|
|
120
|
-
|
|
121
|
-
|
|
129
|
+
/** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
|
|
130
|
+
async resolvePermissions(appRoleId) {
|
|
131
|
+
if (!appRoleId) return [];
|
|
132
|
+
try {
|
|
133
|
+
const appRoleService = this.container.resolve("appRoleModuleService");
|
|
134
|
+
return await appRoleService.getPermissionsForRole(appRoleId);
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
signToken(userId, workspaceId, roles, permissions, secret) {
|
|
140
|
+
const jti = (0, import_crypto.randomUUID)();
|
|
141
|
+
const expiresAt = new Date(Date.now() + JWT_EXPIRES_MS);
|
|
142
|
+
const token = import_jsonwebtoken.default.sign({ sub: userId, workspaceId, roles, permissions, jti }, secret, {
|
|
122
143
|
expiresIn: JWT_EXPIRES_IN
|
|
123
144
|
});
|
|
145
|
+
return { token, jti, expiresAt };
|
|
124
146
|
}
|
|
125
147
|
};
|
|
126
148
|
|
|
@@ -133,25 +155,42 @@ function authenticateJWT(req, res, next) {
|
|
|
133
155
|
res.status(401).json({ error: { message: "Unauthorized \u2014 Bearer token required" } });
|
|
134
156
|
return;
|
|
135
157
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
;
|
|
159
|
+
(async () => {
|
|
160
|
+
let config;
|
|
161
|
+
try {
|
|
162
|
+
const scope = req.scope;
|
|
163
|
+
config = scope.resolve("config");
|
|
164
|
+
} catch {
|
|
165
|
+
res.status(500).json({ error: { message: "Server misconfiguration" } });
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
try {
|
|
169
|
+
const payload = import_jsonwebtoken2.default.verify(token, config.projectConfig.jwtSecret);
|
|
170
|
+
if (payload.jti) {
|
|
171
|
+
try {
|
|
172
|
+
const scope = req.scope;
|
|
173
|
+
const userService = scope.resolve("userModuleService");
|
|
174
|
+
const valid = await userService.isSessionValid(payload.jti);
|
|
175
|
+
if (!valid) {
|
|
176
|
+
res.status(401).json({ error: { message: "Session revoked or expired" } });
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
} catch {
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
req.user = {
|
|
183
|
+
id: payload.sub,
|
|
184
|
+
workspaceId: payload.workspaceId ?? null,
|
|
185
|
+
roles: Array.isArray(payload.roles) ? payload.roles : [],
|
|
186
|
+
permissions: Array.isArray(payload.permissions) ? payload.permissions : [],
|
|
187
|
+
jti: payload.jti ?? null
|
|
188
|
+
};
|
|
189
|
+
next();
|
|
190
|
+
} catch {
|
|
191
|
+
res.status(401).json({ error: { message: "Invalid or expired token" } });
|
|
192
|
+
}
|
|
193
|
+
})();
|
|
155
194
|
}
|
|
156
195
|
|
|
157
196
|
// src/guards.ts
|
|
@@ -162,6 +201,15 @@ function requireRoles(...roles) {
|
|
|
162
201
|
res.status(403).json({ error: { message: "Forbidden" } });
|
|
163
202
|
};
|
|
164
203
|
}
|
|
204
|
+
function requirePermission(...permissions) {
|
|
205
|
+
return (req, res, next) => {
|
|
206
|
+
const userRoles = req.user?.roles ?? [];
|
|
207
|
+
if (userRoles.includes("super-admin")) return next();
|
|
208
|
+
const userPermissions = req.user?.permissions ?? [];
|
|
209
|
+
if (permissions.some((p) => userPermissions.includes(p))) return next();
|
|
210
|
+
res.status(403).json({ error: { message: "Forbidden \u2014 insufficient permissions" } });
|
|
211
|
+
};
|
|
212
|
+
}
|
|
165
213
|
function requireWorkspace(req, res, next) {
|
|
166
214
|
const workspaceId = req.query?.workspace_id ?? req.body?.workspace_id;
|
|
167
215
|
if (workspaceId && req.user?.workspaceId && req.user.workspaceId !== workspaceId) {
|
|
@@ -180,6 +228,7 @@ var index_default = (0, import_framework_utils2.Module)(AUTH_MODULE, {
|
|
|
180
228
|
AUTH_MODULE,
|
|
181
229
|
AuthModuleService,
|
|
182
230
|
authenticateJWT,
|
|
231
|
+
requirePermission,
|
|
183
232
|
requireRoles,
|
|
184
233
|
requireWorkspace
|
|
185
234
|
});
|
package/dist/index.mjs
CHANGED
|
@@ -5,8 +5,10 @@ import { Module } from "@meridianjs/framework-utils";
|
|
|
5
5
|
import { MeridianService } from "@meridianjs/framework-utils";
|
|
6
6
|
import bcrypt from "bcrypt";
|
|
7
7
|
import jwt from "jsonwebtoken";
|
|
8
|
+
import { randomUUID } from "crypto";
|
|
8
9
|
var BCRYPT_ROUNDS = 12;
|
|
9
10
|
var JWT_EXPIRES_IN = "7d";
|
|
11
|
+
var JWT_EXPIRES_MS = 7 * 24 * 60 * 60 * 1e3;
|
|
10
12
|
var AuthModuleService = class extends MeridianService({}) {
|
|
11
13
|
container;
|
|
12
14
|
constructor(container) {
|
|
@@ -35,7 +37,10 @@ var AuthModuleService = class extends MeridianService({}) {
|
|
|
35
37
|
role,
|
|
36
38
|
is_active: true
|
|
37
39
|
});
|
|
38
|
-
const
|
|
40
|
+
const permissions = await this.resolvePermissions(user.app_role_id);
|
|
41
|
+
const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role], permissions, config.projectConfig.jwtSecret);
|
|
42
|
+
await userService.createSession(jti, user.id, expiresAt).catch(() => {
|
|
43
|
+
});
|
|
39
44
|
return {
|
|
40
45
|
user: {
|
|
41
46
|
id: user.id,
|
|
@@ -63,7 +68,10 @@ var AuthModuleService = class extends MeridianService({}) {
|
|
|
63
68
|
}
|
|
64
69
|
await userService.recordLogin(user.id).catch(() => {
|
|
65
70
|
});
|
|
66
|
-
const
|
|
71
|
+
const permissions = await this.resolvePermissions(user.app_role_id);
|
|
72
|
+
const { token, jti, expiresAt } = this.signToken(user.id, null, [user.role ?? "member"], permissions, config.projectConfig.jwtSecret);
|
|
73
|
+
await userService.createSession(jti, user.id, expiresAt).catch(() => {
|
|
74
|
+
});
|
|
67
75
|
return {
|
|
68
76
|
user: {
|
|
69
77
|
id: user.id,
|
|
@@ -78,10 +86,23 @@ var AuthModuleService = class extends MeridianService({}) {
|
|
|
78
86
|
verifyToken(token, secret) {
|
|
79
87
|
return jwt.verify(token, secret);
|
|
80
88
|
}
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
/** Resolve permissions for a given app_role_id — gracefully degrades if module not loaded. */
|
|
90
|
+
async resolvePermissions(appRoleId) {
|
|
91
|
+
if (!appRoleId) return [];
|
|
92
|
+
try {
|
|
93
|
+
const appRoleService = this.container.resolve("appRoleModuleService");
|
|
94
|
+
return await appRoleService.getPermissionsForRole(appRoleId);
|
|
95
|
+
} catch {
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
signToken(userId, workspaceId, roles, permissions, secret) {
|
|
100
|
+
const jti = randomUUID();
|
|
101
|
+
const expiresAt = new Date(Date.now() + JWT_EXPIRES_MS);
|
|
102
|
+
const token = jwt.sign({ sub: userId, workspaceId, roles, permissions, jti }, secret, {
|
|
83
103
|
expiresIn: JWT_EXPIRES_IN
|
|
84
104
|
});
|
|
105
|
+
return { token, jti, expiresAt };
|
|
85
106
|
}
|
|
86
107
|
};
|
|
87
108
|
|
|
@@ -94,25 +115,42 @@ function authenticateJWT(req, res, next) {
|
|
|
94
115
|
res.status(401).json({ error: { message: "Unauthorized \u2014 Bearer token required" } });
|
|
95
116
|
return;
|
|
96
117
|
}
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
118
|
+
;
|
|
119
|
+
(async () => {
|
|
120
|
+
let config;
|
|
121
|
+
try {
|
|
122
|
+
const scope = req.scope;
|
|
123
|
+
config = scope.resolve("config");
|
|
124
|
+
} catch {
|
|
125
|
+
res.status(500).json({ error: { message: "Server misconfiguration" } });
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
const payload = jwt2.verify(token, config.projectConfig.jwtSecret);
|
|
130
|
+
if (payload.jti) {
|
|
131
|
+
try {
|
|
132
|
+
const scope = req.scope;
|
|
133
|
+
const userService = scope.resolve("userModuleService");
|
|
134
|
+
const valid = await userService.isSessionValid(payload.jti);
|
|
135
|
+
if (!valid) {
|
|
136
|
+
res.status(401).json({ error: { message: "Session revoked or expired" } });
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
} catch {
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
req.user = {
|
|
143
|
+
id: payload.sub,
|
|
144
|
+
workspaceId: payload.workspaceId ?? null,
|
|
145
|
+
roles: Array.isArray(payload.roles) ? payload.roles : [],
|
|
146
|
+
permissions: Array.isArray(payload.permissions) ? payload.permissions : [],
|
|
147
|
+
jti: payload.jti ?? null
|
|
148
|
+
};
|
|
149
|
+
next();
|
|
150
|
+
} catch {
|
|
151
|
+
res.status(401).json({ error: { message: "Invalid or expired token" } });
|
|
152
|
+
}
|
|
153
|
+
})();
|
|
116
154
|
}
|
|
117
155
|
|
|
118
156
|
// src/guards.ts
|
|
@@ -123,6 +161,15 @@ function requireRoles(...roles) {
|
|
|
123
161
|
res.status(403).json({ error: { message: "Forbidden" } });
|
|
124
162
|
};
|
|
125
163
|
}
|
|
164
|
+
function requirePermission(...permissions) {
|
|
165
|
+
return (req, res, next) => {
|
|
166
|
+
const userRoles = req.user?.roles ?? [];
|
|
167
|
+
if (userRoles.includes("super-admin")) return next();
|
|
168
|
+
const userPermissions = req.user?.permissions ?? [];
|
|
169
|
+
if (permissions.some((p) => userPermissions.includes(p))) return next();
|
|
170
|
+
res.status(403).json({ error: { message: "Forbidden \u2014 insufficient permissions" } });
|
|
171
|
+
};
|
|
172
|
+
}
|
|
126
173
|
function requireWorkspace(req, res, next) {
|
|
127
174
|
const workspaceId = req.query?.workspace_id ?? req.body?.workspace_id;
|
|
128
175
|
if (workspaceId && req.user?.workspaceId && req.user.workspaceId !== workspaceId) {
|
|
@@ -141,6 +188,7 @@ export {
|
|
|
141
188
|
AuthModuleService,
|
|
142
189
|
authenticateJWT,
|
|
143
190
|
index_default as default,
|
|
191
|
+
requirePermission,
|
|
144
192
|
requireRoles,
|
|
145
193
|
requireWorkspace
|
|
146
194
|
};
|