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.
Files changed (74) hide show
  1. package/package.json +6 -5
  2. package/src/auth/Auth.js +13 -8
  3. package/src/auth/AuthController.js +45 -134
  4. package/src/auth/AuthMiddleware.js +12 -23
  5. package/src/auth/AuthUser.js +98 -0
  6. package/src/auth/RoleMiddleware.js +7 -17
  7. package/src/cli.js +1 -1
  8. package/src/commands/migrate.js +46 -31
  9. package/src/commands/serve.js +238 -38
  10. package/src/container/AppInitializer.js +158 -0
  11. package/src/container/Application.js +288 -183
  12. package/src/container/HttpServer.js +156 -0
  13. package/src/container/MillasApp.js +23 -280
  14. package/src/container/MillasConfig.js +163 -0
  15. package/src/controller/Controller.js +79 -300
  16. package/src/core/auth.js +9 -0
  17. package/src/core/db.js +8 -0
  18. package/src/core/foundation.js +67 -0
  19. package/src/core/http.js +11 -0
  20. package/src/core/mail.js +6 -0
  21. package/src/core/queue.js +7 -0
  22. package/src/core/validation.js +29 -0
  23. package/src/errors/ErrorRenderer.js +640 -0
  24. package/src/facades/Admin.js +49 -0
  25. package/src/facades/Auth.js +29 -0
  26. package/src/facades/Cache.js +28 -0
  27. package/src/facades/Database.js +43 -0
  28. package/src/facades/Events.js +25 -0
  29. package/src/facades/Facade.js +197 -0
  30. package/src/facades/Http.js +51 -0
  31. package/src/facades/Log.js +32 -0
  32. package/src/facades/Mail.js +35 -0
  33. package/src/facades/Queue.js +30 -0
  34. package/src/facades/Storage.js +25 -0
  35. package/src/facades/Url.js +53 -0
  36. package/src/http/HttpClient.js +673 -0
  37. package/src/http/MillasRequest.js +253 -0
  38. package/src/http/MillasResponse.js +196 -0
  39. package/src/http/RequestContext.js +176 -0
  40. package/src/http/ResponseDispatcher.js +51 -0
  41. package/src/http/UrlGenerator.js +375 -0
  42. package/src/http/WelcomePage.js +273 -0
  43. package/src/http/adapters/ExpressAdapter.js +315 -0
  44. package/src/http/adapters/HttpAdapter.js +168 -0
  45. package/src/http/adapters/index.js +9 -0
  46. package/src/http/helpers.js +164 -0
  47. package/src/http/index.js +13 -0
  48. package/src/index.js +5 -91
  49. package/src/logger/formatters/PrettyFormatter.js +15 -5
  50. package/src/logger/internal.js +76 -0
  51. package/src/logger/patchConsole.js +145 -0
  52. package/src/middleware/CorsMiddleware.js +22 -30
  53. package/src/middleware/LogMiddleware.js +27 -59
  54. package/src/middleware/Middleware.js +24 -15
  55. package/src/middleware/MiddlewarePipeline.js +30 -67
  56. package/src/middleware/MiddlewareRegistry.js +106 -0
  57. package/src/middleware/ThrottleMiddleware.js +22 -26
  58. package/src/orm/fields/index.js +124 -56
  59. package/src/orm/migration/ModelInspector.js +339 -336
  60. package/src/orm/model/Model.js +96 -6
  61. package/src/orm/query/QueryBuilder.js +141 -3
  62. package/src/providers/AuthServiceProvider.js +9 -5
  63. package/src/providers/CacheStorageServiceProvider.js +3 -1
  64. package/src/providers/EventServiceProvider.js +2 -1
  65. package/src/providers/LogServiceProvider.js +88 -17
  66. package/src/providers/MailServiceProvider.js +3 -2
  67. package/src/providers/ProviderRegistry.js +14 -1
  68. package/src/providers/QueueServiceProvider.js +3 -2
  69. package/src/providers/ServiceProvider.js +40 -8
  70. package/src/router/Router.js +121 -222
  71. package/src/scaffold/maker.js +24 -59
  72. package/src/scaffold/templates.js +21 -19
  73. package/src/validation/BaseValidator.js +193 -0
  74. 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.11",
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
- "./src": "./src/index.js",
9
- "./src/*": "./src/*.js",
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
- return {
234
- id: user.id,
235
- sub: user.id,
236
- email: user.email,
237
- role: user.role || null,
238
- iat: Math.floor(Date.now() / 1000),
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
- * Register its routes in routes/api.js:
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
- * POST /auth/register
29
- * Body: { name, email, password, password_confirmation? }
30
- */
31
- async register(req, res) {
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(res, {
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
- * POST /auth/login
50
- * Body: { email, password }
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(email, password);
38
+ const { user, token, refreshToken } = await Auth.login(emailVal, password);
59
39
 
60
- return this.ok(res, {
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
- * POST /auth/logout
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
- * GET /auth/me
81
- * Header: Authorization: Bearer <token>
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
- * POST /auth/refresh
95
- * Body: { refresh_token }
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
- // Verify the refresh token
103
- let payload;
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
- * POST /auth/forgot-password
124
- * Body: { email }
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
- // Always return 200 to prevent email enumeration
132
- try {
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
- * POST /auth/reset-password
154
- * Body: { token, password }
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
- if (!user || user.email !== payload.email) {
168
- return this.badRequest(res, 'Invalid reset token');
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
- const obj = user.toJSON ? user.toJSON() : { ...user };
182
- delete obj.password;
183
- delete obj.remember_token;
184
- return obj;
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 from the database,
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
- * Register in bootstrap/app.js:
22
- * app.middleware('auth', AuthMiddleware)
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(req, res, next) {
29
- const header = req.headers['authorization'];
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 = user;
60
- req.token = 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 route access to users with specific roles.
10
- * Must be used AFTER AuthMiddleware (requires req.user).
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(req, res, next) {
28
- if (!req.user) {
29
- throw new HttpError(401, 'Unauthenticated — run AuthMiddleware before RoleMiddleware');
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 = req.user.role || null;
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.0');
10
+ .version('0.2.12-beta-1');
11
11
 
12
12
  // Load all command modules
13
13
  require('./commands/new')(program);