millas 0.2.11 → 0.2.12-beta-1
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/package.json +6 -5
- package/src/auth/Auth.js +13 -8
- package/src/auth/AuthController.js +45 -134
- package/src/auth/AuthMiddleware.js +12 -23
- package/src/auth/AuthUser.js +98 -0
- package/src/auth/RoleMiddleware.js +7 -17
- package/src/cli.js +1 -1
- package/src/commands/migrate.js +46 -31
- package/src/commands/serve.js +238 -38
- package/src/container/AppInitializer.js +158 -0
- package/src/container/Application.js +288 -183
- package/src/container/HttpServer.js +156 -0
- package/src/container/MillasApp.js +23 -280
- package/src/container/MillasConfig.js +163 -0
- package/src/controller/Controller.js +79 -300
- package/src/core/auth.js +9 -0
- package/src/core/db.js +8 -0
- package/src/core/foundation.js +67 -0
- package/src/core/http.js +11 -0
- package/src/core/mail.js +6 -0
- package/src/core/queue.js +7 -0
- package/src/core/validation.js +29 -0
- package/src/errors/ErrorRenderer.js +640 -0
- package/src/facades/Admin.js +49 -0
- package/src/facades/Auth.js +29 -0
- package/src/facades/Cache.js +28 -0
- package/src/facades/Database.js +43 -0
- package/src/facades/Events.js +25 -0
- package/src/facades/Facade.js +197 -0
- package/src/facades/Http.js +51 -0
- package/src/facades/Log.js +32 -0
- package/src/facades/Mail.js +35 -0
- package/src/facades/Queue.js +30 -0
- package/src/facades/Storage.js +25 -0
- package/src/facades/Url.js +53 -0
- package/src/http/HttpClient.js +673 -0
- package/src/http/MillasRequest.js +253 -0
- package/src/http/MillasResponse.js +196 -0
- package/src/http/RequestContext.js +176 -0
- package/src/http/ResponseDispatcher.js +51 -0
- package/src/http/UrlGenerator.js +375 -0
- package/src/http/WelcomePage.js +273 -0
- package/src/http/adapters/ExpressAdapter.js +315 -0
- package/src/http/adapters/HttpAdapter.js +168 -0
- package/src/http/adapters/index.js +9 -0
- package/src/http/helpers.js +164 -0
- package/src/http/index.js +13 -0
- package/src/index.js +5 -91
- package/src/logger/formatters/PrettyFormatter.js +15 -5
- package/src/logger/internal.js +76 -0
- package/src/logger/patchConsole.js +145 -0
- package/src/middleware/CorsMiddleware.js +22 -30
- package/src/middleware/LogMiddleware.js +27 -59
- package/src/middleware/Middleware.js +24 -15
- package/src/middleware/MiddlewarePipeline.js +30 -67
- package/src/middleware/MiddlewareRegistry.js +106 -0
- package/src/middleware/ThrottleMiddleware.js +22 -26
- package/src/orm/fields/index.js +124 -56
- package/src/orm/migration/ModelInspector.js +339 -336
- package/src/orm/model/Model.js +96 -6
- package/src/orm/query/QueryBuilder.js +141 -3
- package/src/providers/AuthServiceProvider.js +9 -5
- package/src/providers/CacheStorageServiceProvider.js +3 -1
- package/src/providers/EventServiceProvider.js +2 -1
- package/src/providers/LogServiceProvider.js +88 -17
- package/src/providers/MailServiceProvider.js +3 -2
- package/src/providers/ProviderRegistry.js +14 -1
- package/src/providers/QueueServiceProvider.js +3 -2
- package/src/providers/ServiceProvider.js +40 -8
- package/src/router/Router.js +121 -222
- package/src/scaffold/maker.js +24 -59
- package/src/scaffold/templates.js +21 -19
- package/src/validation/BaseValidator.js +193 -0
- package/src/validation/Validator.js +680 -0
package/package.json
CHANGED
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "millas",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12-beta-1",
|
|
4
4
|
"description": "A modern batteries-included backend framework for Node.js — built on Express, inspired by Laravel, Django, and FastAPI",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": "./src/index.js",
|
|
8
|
-
"./
|
|
9
|
-
"./
|
|
10
|
-
"./bin/*": "./bin/*.js"
|
|
8
|
+
"./core/*": "./src/core/*.js",
|
|
9
|
+
"./facades/*": "./src/facades/*.js"
|
|
11
10
|
},
|
|
12
11
|
"bin": {
|
|
13
12
|
"millas": "./bin/millas.js"
|
|
@@ -39,6 +38,7 @@
|
|
|
39
38
|
"dependencies": {
|
|
40
39
|
"bcryptjs": "3.0.2",
|
|
41
40
|
"chalk": "4.1.2",
|
|
41
|
+
"chokidar": "^3.6.0",
|
|
42
42
|
"commander": "^11.0.0",
|
|
43
43
|
"fs-extra": "^11.0.0",
|
|
44
44
|
"inquirer": "8.2.6",
|
|
@@ -46,7 +46,8 @@
|
|
|
46
46
|
"knex": "^3.1.0",
|
|
47
47
|
"nodemailer": "^6.9.0",
|
|
48
48
|
"nunjucks": "^3.2.4",
|
|
49
|
-
"ora": "5.4.1"
|
|
49
|
+
"ora": "5.4.1",
|
|
50
|
+
"sqlite3": "^5.1.7"
|
|
50
51
|
},
|
|
51
52
|
"peerDependencies": {
|
|
52
53
|
"express": "^4.18.0"
|
package/src/auth/Auth.js
CHANGED
|
@@ -230,13 +230,18 @@ class Auth {
|
|
|
230
230
|
}
|
|
231
231
|
|
|
232
232
|
_buildTokenPayload(user) {
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
233
|
+
// If the model defines tokenPayload(), use it — allows custom JWT claims.
|
|
234
|
+
// Otherwise fall back to the standard shape.
|
|
235
|
+
const base = typeof user.tokenPayload === 'function'
|
|
236
|
+
? user.tokenPayload()
|
|
237
|
+
: {
|
|
238
|
+
id: user.id,
|
|
239
|
+
sub: user.id,
|
|
240
|
+
email: user.email,
|
|
241
|
+
role: user.role || null,
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
return { ...base, iat: Math.floor(Date.now() / 1000) };
|
|
240
245
|
}
|
|
241
246
|
|
|
242
247
|
_requireUserModel() {
|
|
@@ -251,4 +256,4 @@ class Auth {
|
|
|
251
256
|
|
|
252
257
|
// Singleton facade
|
|
253
258
|
module.exports = new Auth();
|
|
254
|
-
module.exports.Auth = Auth;
|
|
259
|
+
module.exports.Auth = Auth;
|
|
@@ -2,62 +2,42 @@
|
|
|
2
2
|
|
|
3
3
|
const Controller = require('../controller/Controller');
|
|
4
4
|
const Auth = require('./Auth');
|
|
5
|
+
const { string, email } = require('../validation/Validator');
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* AuthController
|
|
8
9
|
*
|
|
9
|
-
* Drop-in authentication controller.
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* const AuthController = require('millas/src/auth/AuthController');
|
|
13
|
-
*
|
|
14
|
-
* Route.post('/auth/register', AuthController, 'register');
|
|
15
|
-
* Route.post('/auth/login', AuthController, 'login');
|
|
16
|
-
* Route.post('/auth/logout', AuthController, 'logout');
|
|
17
|
-
* Route.get('/auth/me', AuthController, 'me');
|
|
18
|
-
* Route.post('/auth/refresh', AuthController, 'refresh');
|
|
19
|
-
* Route.post('/auth/forgot-password', AuthController, 'forgotPassword');
|
|
20
|
-
* Route.post('/auth/reset-password', AuthController, 'resetPassword');
|
|
21
|
-
*
|
|
22
|
-
* Or use the convenience helper:
|
|
23
|
-
* Route.auth() — registers all routes above under /auth
|
|
10
|
+
* Drop-in authentication controller using the new ctx signature.
|
|
11
|
+
* All methods receive RequestContext and return MillasResponse.
|
|
24
12
|
*/
|
|
25
13
|
class AuthController extends Controller {
|
|
26
14
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
const data = await this.validate(req, {
|
|
33
|
-
name: 'required|string|min:2|max:100',
|
|
34
|
-
email: 'required|email',
|
|
35
|
-
password: 'required|string|min:8',
|
|
15
|
+
async register({ body }) {
|
|
16
|
+
const data = await body.validate({
|
|
17
|
+
name: string().required().min(2).max(100),
|
|
18
|
+
email: email().required(),
|
|
19
|
+
password: string().required().min(8),
|
|
36
20
|
});
|
|
37
21
|
|
|
38
22
|
const user = await Auth.register(data);
|
|
39
23
|
const token = Auth.issueToken(user);
|
|
40
24
|
|
|
41
|
-
return this.created(
|
|
25
|
+
return this.created({
|
|
42
26
|
message: 'Registration successful',
|
|
43
27
|
user: this._safeUser(user),
|
|
44
28
|
token,
|
|
45
29
|
});
|
|
46
30
|
}
|
|
47
31
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
async login(req, res) {
|
|
53
|
-
const { email, password } = await this.validate(req, {
|
|
54
|
-
email: 'required|email',
|
|
55
|
-
password: 'required|string',
|
|
32
|
+
async login({ body }) {
|
|
33
|
+
const { email: emailVal, password } = await body.validate({
|
|
34
|
+
email: email().required(),
|
|
35
|
+
password: string().required(),
|
|
56
36
|
});
|
|
57
37
|
|
|
58
|
-
const { user, token, refreshToken } = await Auth.login(
|
|
38
|
+
const { user, token, refreshToken } = await Auth.login(emailVal, password);
|
|
59
39
|
|
|
60
|
-
return this.ok(
|
|
40
|
+
return this.ok({
|
|
61
41
|
message: 'Login successful',
|
|
62
42
|
user: this._safeUser(user),
|
|
63
43
|
token,
|
|
@@ -65,124 +45,55 @@ class AuthController extends Controller {
|
|
|
65
45
|
});
|
|
66
46
|
}
|
|
67
47
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
* Header: Authorization: Bearer <token>
|
|
71
|
-
*
|
|
72
|
-
* JWT is stateless — logout just instructs the client to discard the token.
|
|
73
|
-
* Phase 11 (cache) will add token blocklisting.
|
|
74
|
-
*/
|
|
75
|
-
async logout(req, res) {
|
|
76
|
-
return this.ok(res, { message: 'Logged out successfully' });
|
|
48
|
+
async logout() {
|
|
49
|
+
return this.ok({ message: 'Logged out successfully' });
|
|
77
50
|
}
|
|
78
51
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
async me(req, res) {
|
|
84
|
-
try {
|
|
85
|
-
const user = await Auth.userOrFail(req);
|
|
86
|
-
return this.ok(res, { user: this._safeUser(user) });
|
|
87
|
-
} catch (err) {
|
|
88
|
-
if (err.status === 401) return this.unauthorized(res, err.message);
|
|
89
|
-
return this.unauthorized(res, 'Unauthenticated');
|
|
52
|
+
async me({ user }) {
|
|
53
|
+
if (!user) {
|
|
54
|
+
const HttpError = require('../errors/HttpError');
|
|
55
|
+
throw new HttpError(401, 'Unauthenticated');
|
|
90
56
|
}
|
|
57
|
+
return this.ok({ user: this._safeUser(user) });
|
|
91
58
|
}
|
|
92
59
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
*/
|
|
97
|
-
async refresh(req, res) {
|
|
98
|
-
const { refresh_token } = await this.validate(req, {
|
|
99
|
-
refresh_token: 'required|string',
|
|
60
|
+
async refresh({ body }) {
|
|
61
|
+
const { refresh_token } = await body.validate({
|
|
62
|
+
refresh_token: string().required(),
|
|
100
63
|
});
|
|
101
64
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
try {
|
|
105
|
-
payload = Auth.verify(refresh_token);
|
|
106
|
-
} catch {
|
|
107
|
-
return this.unauthorized(res, 'Invalid or expired refresh token');
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
const user = await Auth.user({ headers: { authorization: `Bearer ${refresh_token}` } });
|
|
111
|
-
if (!user) return this.unauthorized(res, 'User not found');
|
|
112
|
-
|
|
113
|
-
const newToken = Auth.issueToken(user);
|
|
114
|
-
const newRefreshToken = Auth.issueToken(user, { expiresIn: '30d' });
|
|
115
|
-
|
|
116
|
-
return this.ok(res, {
|
|
117
|
-
token: newToken,
|
|
118
|
-
refresh_token: newRefreshToken,
|
|
119
|
-
});
|
|
65
|
+
const tokens = await Auth.refresh(refresh_token);
|
|
66
|
+
return this.ok(tokens);
|
|
120
67
|
}
|
|
121
68
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
*/
|
|
126
|
-
async forgotPassword(req, res) {
|
|
127
|
-
const { email } = await this.validate(req, {
|
|
128
|
-
email: 'required|email',
|
|
69
|
+
async forgotPassword({ body }) {
|
|
70
|
+
const { email: emailVal } = await body.validate({
|
|
71
|
+
email: email().required(),
|
|
129
72
|
});
|
|
130
73
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
const user = await Auth.user({ headers: {} });
|
|
134
|
-
if (user) {
|
|
135
|
-
const resetToken = Auth.generateResetToken(user);
|
|
136
|
-
// Phase 8: Mail.send({ to: email, template: 'password-reset', data: { resetToken } })
|
|
137
|
-
// For now, return token in dev mode only
|
|
138
|
-
if (process.env.APP_ENV !== 'production') {
|
|
139
|
-
return this.ok(res, {
|
|
140
|
-
message: 'Reset link sent (dev mode — token exposed)',
|
|
141
|
-
reset_token: resetToken,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
}
|
|
145
|
-
} catch { /* silent */ }
|
|
146
|
-
|
|
147
|
-
return this.ok(res, {
|
|
148
|
-
message: 'If that email exists, a reset link has been sent.',
|
|
149
|
-
});
|
|
74
|
+
await Auth.sendPasswordResetEmail(emailVal);
|
|
75
|
+
return this.ok({ message: 'Password reset email sent' });
|
|
150
76
|
}
|
|
151
77
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
async resetPassword(req, res) {
|
|
157
|
-
const { token, password } = await this.validate(req, {
|
|
158
|
-
token: 'required|string',
|
|
159
|
-
password: 'required|string|min:8',
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
const payload = Auth.verifyResetToken(token);
|
|
163
|
-
const user = await Auth.user({
|
|
164
|
-
headers: { authorization: `Bearer ${token}` },
|
|
78
|
+
async resetPassword({ body }) {
|
|
79
|
+
const { token, password } = await body.validate({
|
|
80
|
+
token: string().required(),
|
|
81
|
+
password: string().required().min(8).confirmed(),
|
|
165
82
|
});
|
|
166
83
|
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
const hashed = await Auth.hashPassword(password);
|
|
172
|
-
await user.update({ password: hashed });
|
|
173
|
-
|
|
174
|
-
return this.ok(res, { message: 'Password reset successfully' });
|
|
84
|
+
await Auth.resetPassword(token, password);
|
|
85
|
+
return this.ok({ message: 'Password reset successfully' });
|
|
175
86
|
}
|
|
176
87
|
|
|
177
|
-
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
178
|
-
|
|
179
88
|
_safeUser(user) {
|
|
180
89
|
if (!user) return null;
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
90
|
+
// Use the model's toSafeObject() if defined (AuthUser and subclasses provide this)
|
|
91
|
+
if (typeof user.toSafeObject === 'function') return user.toSafeObject();
|
|
92
|
+
const data = user.toJSON ? user.toJSON() : { ...user };
|
|
93
|
+
delete data.password;
|
|
94
|
+
delete data.remember_token;
|
|
95
|
+
return data;
|
|
185
96
|
}
|
|
186
97
|
}
|
|
187
98
|
|
|
188
|
-
module.exports = AuthController;
|
|
99
|
+
module.exports = AuthController;
|
|
@@ -8,25 +8,15 @@ const Auth = require('./Auth');
|
|
|
8
8
|
* AuthMiddleware
|
|
9
9
|
*
|
|
10
10
|
* Guards routes from unauthenticated access using JWT.
|
|
11
|
-
*
|
|
12
11
|
* Reads the Bearer token from the Authorization header,
|
|
13
|
-
* verifies it, loads the user
|
|
14
|
-
* and attaches them to req.user.
|
|
15
|
-
*
|
|
16
|
-
* Throws 401 if:
|
|
17
|
-
* - No Authorization header
|
|
18
|
-
* - Token is malformed / expired
|
|
19
|
-
* - User no longer exists in DB
|
|
12
|
+
* verifies it, loads the user, and attaches them to req.user.
|
|
20
13
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
* Apply to routes:
|
|
25
|
-
* Route.prefix('/api').middleware(['auth']).group(() => { ... })
|
|
14
|
+
* Uses the Millas middleware signature: handle(req, next)
|
|
15
|
+
* No Express res — returns a MillasResponse or calls next().
|
|
26
16
|
*/
|
|
27
17
|
class AuthMiddleware extends Middleware {
|
|
28
|
-
async handle(
|
|
29
|
-
const header =
|
|
18
|
+
async handle({ headers, req }, next) {
|
|
19
|
+
const header = headers.authorization || headers.Authorization;
|
|
30
20
|
|
|
31
21
|
if (!header) {
|
|
32
22
|
throw new HttpError(401, 'Authorization header missing');
|
|
@@ -41,26 +31,25 @@ class AuthMiddleware extends Middleware {
|
|
|
41
31
|
throw new HttpError(401, 'Token is empty');
|
|
42
32
|
}
|
|
43
33
|
|
|
44
|
-
// Verify token — throws 401 if expired or invalid
|
|
45
34
|
const payload = Auth.verify(token);
|
|
46
35
|
|
|
47
|
-
// Load user from DB
|
|
48
36
|
let user;
|
|
49
37
|
try {
|
|
50
|
-
user = await Auth.user(req);
|
|
38
|
+
user = await Auth.user(req.raw);
|
|
51
39
|
} catch {
|
|
52
40
|
throw new HttpError(401, 'Authentication service not configured');
|
|
53
41
|
}
|
|
42
|
+
|
|
54
43
|
if (!user) {
|
|
55
44
|
throw new HttpError(401, 'User not found or has been deleted');
|
|
56
45
|
}
|
|
57
46
|
|
|
58
|
-
// Attach to request
|
|
59
|
-
req.user
|
|
60
|
-
req.token
|
|
61
|
-
req.tokenPayload = payload;
|
|
47
|
+
// Attach to the underlying request so downstream handlers see req.user
|
|
48
|
+
req.raw.user = user;
|
|
49
|
+
req.raw.token = token;
|
|
50
|
+
req.raw.tokenPayload = payload;
|
|
62
51
|
|
|
63
|
-
next();
|
|
52
|
+
return next();
|
|
64
53
|
}
|
|
65
54
|
}
|
|
66
55
|
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Model = require('../orm/model/Model');
|
|
4
|
+
const fields = require('../orm/fields/index').fields;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* AuthUser
|
|
8
|
+
*
|
|
9
|
+
* Base model for authentication. Ships with Millas.
|
|
10
|
+
* Covers the exact contract that Auth, AuthMiddleware, and AuthController expect:
|
|
11
|
+
* - email (unique, required for login)
|
|
12
|
+
* - password (hashed by Auth.register / Auth.hashPassword)
|
|
13
|
+
* - role (read by RoleMiddleware)
|
|
14
|
+
*
|
|
15
|
+
* ── Extending ────────────────────────────────────────────────────────────────
|
|
16
|
+
*
|
|
17
|
+
* Extend this instead of writing a User model from scratch.
|
|
18
|
+
* Add your own fields on top — all Auth behaviour is inherited.
|
|
19
|
+
*
|
|
20
|
+
* class User extends AuthUser {
|
|
21
|
+
* static table = 'users';
|
|
22
|
+
* static fields = {
|
|
23
|
+
* ...AuthUser.fields,
|
|
24
|
+
* phone: fields.string({ nullable: true }),
|
|
25
|
+
* avatar_url: fields.string({ nullable: true }),
|
|
26
|
+
* bio: fields.text({ nullable: true }),
|
|
27
|
+
* };
|
|
28
|
+
* }
|
|
29
|
+
*
|
|
30
|
+
* ── Customising the token payload ─────────────────────────────────────────
|
|
31
|
+
*
|
|
32
|
+
* Override tokenPayload() to add custom claims to the JWT:
|
|
33
|
+
*
|
|
34
|
+
* class User extends AuthUser {
|
|
35
|
+
* tokenPayload() {
|
|
36
|
+
* return {
|
|
37
|
+
* ...super.tokenPayload(),
|
|
38
|
+
* plan: this.plan,
|
|
39
|
+
* orgId: this.org_id,
|
|
40
|
+
* };
|
|
41
|
+
* }
|
|
42
|
+
* }
|
|
43
|
+
*
|
|
44
|
+
* ── Customising register / login hooks ────────────────────────────────────
|
|
45
|
+
*
|
|
46
|
+
* Override static hooks to run logic around auth operations:
|
|
47
|
+
*
|
|
48
|
+
* class User extends AuthUser {
|
|
49
|
+
* static async afterCreate(instance) {
|
|
50
|
+
* await emit('user.registered', { user: instance });
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
*/
|
|
54
|
+
class AuthUser extends Model {
|
|
55
|
+
static table = 'users';
|
|
56
|
+
|
|
57
|
+
static fields = {
|
|
58
|
+
id: fields.id(),
|
|
59
|
+
name: fields.string({ max: 100 }),
|
|
60
|
+
email: fields.string({ unique: true }),
|
|
61
|
+
password: fields.string(),
|
|
62
|
+
role: fields.enum(['admin', 'user'], { default: 'user' }),
|
|
63
|
+
created_at: fields.timestamp(),
|
|
64
|
+
updated_at: fields.timestamp(),
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// ── Auth contract helpers ──────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* The fields to include in the JWT payload.
|
|
71
|
+
* Override to add custom claims.
|
|
72
|
+
*
|
|
73
|
+
* @returns {object}
|
|
74
|
+
*/
|
|
75
|
+
tokenPayload() {
|
|
76
|
+
return {
|
|
77
|
+
id: this.id,
|
|
78
|
+
sub: this.id,
|
|
79
|
+
email: this.email,
|
|
80
|
+
role: this.role || null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Safe representation — strips sensitive fields.
|
|
86
|
+
* Used by AuthController._safeUser().
|
|
87
|
+
*
|
|
88
|
+
* @returns {object}
|
|
89
|
+
*/
|
|
90
|
+
toSafeObject() {
|
|
91
|
+
const data = { ...this };
|
|
92
|
+
delete data.password;
|
|
93
|
+
delete data.remember_token;
|
|
94
|
+
return data;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
module.exports = AuthUser;
|
|
@@ -6,38 +6,28 @@ const HttpError = require('../errors/HttpError');
|
|
|
6
6
|
/**
|
|
7
7
|
* RoleMiddleware
|
|
8
8
|
*
|
|
9
|
-
* Restricts
|
|
10
|
-
* Must
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* middlewareRegistry.register('admin', new RoleMiddleware(['admin']));
|
|
14
|
-
* middlewareRegistry.register('staff', new RoleMiddleware(['admin', 'staff']));
|
|
15
|
-
*
|
|
16
|
-
* Route.prefix('/admin').middleware(['auth', 'admin']).group(() => { ... });
|
|
9
|
+
* Restricts access to users with specific roles.
|
|
10
|
+
* Must run after AuthMiddleware (requires ctx.user).
|
|
17
11
|
*/
|
|
18
12
|
class RoleMiddleware extends Middleware {
|
|
19
|
-
/**
|
|
20
|
-
* @param {string[]} roles — list of allowed role values
|
|
21
|
-
*/
|
|
22
13
|
constructor(roles = []) {
|
|
23
14
|
super();
|
|
24
15
|
this.roles = Array.isArray(roles) ? roles : [roles];
|
|
25
16
|
}
|
|
26
17
|
|
|
27
|
-
async handle(
|
|
28
|
-
if (!
|
|
29
|
-
throw new HttpError(401, 'Unauthenticated —
|
|
18
|
+
async handle({ user }, next) {
|
|
19
|
+
if (!user) {
|
|
20
|
+
throw new HttpError(401, 'Unauthenticated — AuthMiddleware must run first');
|
|
30
21
|
}
|
|
31
22
|
|
|
32
|
-
const userRole =
|
|
33
|
-
|
|
23
|
+
const userRole = user.role || null;
|
|
34
24
|
if (!userRole || !this.roles.includes(userRole)) {
|
|
35
25
|
throw new HttpError(403,
|
|
36
26
|
`Access denied. Required role: ${this.roles.join(' or ')}`
|
|
37
27
|
);
|
|
38
28
|
}
|
|
39
29
|
|
|
40
|
-
next();
|
|
30
|
+
return next();
|
|
41
31
|
}
|
|
42
32
|
}
|
|
43
33
|
|
package/src/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ const program = new Command();
|
|
|
7
7
|
program
|
|
8
8
|
.name('millas')
|
|
9
9
|
.description(chalk.cyan('⚡ Millas — A modern batteries-included Node.js framework'))
|
|
10
|
-
.version('0.1
|
|
10
|
+
.version('0.2.12-beta-1');
|
|
11
11
|
|
|
12
12
|
// Load all command modules
|
|
13
13
|
require('./commands/new')(program);
|