@seip/blue-bird 0.7.5 → 0.8.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.
Files changed (42) hide show
  1. package/.env_example +34 -36
  2. package/AGENTS.md +174 -241
  3. package/LICENSE +21 -21
  4. package/README.md +312 -343
  5. package/{index.js → backend/index.js} +22 -30
  6. package/backend/routes/api.js +57 -57
  7. package/core/app.js +338 -402
  8. package/core/auth.js +262 -256
  9. package/core/cache.js +174 -174
  10. package/core/cli/docker.js +488 -370
  11. package/core/cli/init.js +332 -238
  12. package/core/cli/route.js +42 -42
  13. package/core/config.js +52 -52
  14. package/core/database.js +263 -182
  15. package/core/debug.js +248 -248
  16. package/core/logger.js +115 -115
  17. package/core/middleware.js +27 -27
  18. package/core/router.js +144 -144
  19. package/core/swagger.js +40 -40
  20. package/core/upload.js +77 -77
  21. package/core/validate.js +380 -380
  22. package/docker/Dockerfile +16 -16
  23. package/docker/docker-compose.dev.yml +6 -0
  24. package/docker/docker-compose.mysql.yml +92 -0
  25. package/docker/docker-compose.none.yml +68 -0
  26. package/docker/docker-compose.postgres.yml +93 -0
  27. package/docker/nginx.conf +98 -106
  28. package/docker-compose.yml +92 -93
  29. package/frontend/about.html +98 -0
  30. package/frontend/css/app.css +0 -0
  31. package/frontend/favicon.ico +0 -0
  32. package/frontend/index.html +141 -0
  33. package/frontend/js/bundle.js +8 -0
  34. package/package.json +64 -72
  35. package/backend/logs/2026-07-14/info.log +0 -48
  36. package/frontend/astro.config.mjs +0 -35
  37. package/frontend/public/css/app.css +0 -319
  38. package/frontend/public/favicon.ico +0 -0
  39. package/frontend/src/http/api.js +0 -29
  40. package/frontend/src/layouts/Layout.astro +0 -20
  41. package/frontend/src/pages/about.astro +0 -54
  42. package/frontend/src/pages/index.astro +0 -110
package/core/auth.js CHANGED
@@ -1,256 +1,262 @@
1
- import jwt from "jsonwebtoken";
2
- import crypto from "node:crypto";
3
- import Config from "./config.js";
4
- import { getRedisClient } from "./cache.js";
5
-
6
- const propsConfig = Config.props();
7
- const jwtSecret = propsConfig.jwtSecret;
8
- const production = !propsConfig.debug;
9
- /**
10
- * Auth class to handle JWT generation, verification and protection with AES-256-GCM encryption.
11
- */
12
- class Auth {
13
- /**
14
- * Encrypts a payload using AES-256-GCM.
15
- * @param {Object} payload - The data to encrypt.
16
- * @param {string} secret - The secret key for encryption.
17
- * @returns {string} The encrypted string in format iv:tag:encrypted.
18
- */
19
- static encrypt(payload, secret) {
20
- const iv = crypto.randomBytes(12);
21
- const key = crypto.createHash("sha256").update(secret).digest();
22
- const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
23
- let encrypted = cipher.update(JSON.stringify(payload), "utf8", "hex");
24
- encrypted += cipher.final("hex");
25
- const tag = cipher.getAuthTag().toString("hex");
26
- return `${iv.toString("hex")}:${tag}:${encrypted}`;
27
- }
28
-
29
- /**
30
- * Decrypts a payload using AES-256-GCM.
31
- * @param {string} data - The encrypted string in format iv:tag:encrypted.
32
- * @param {string} secret - The secret key for decryption.
33
- * @returns {Object|null} The decrypted object or null if failed.
34
- */
35
- static decrypt(data, secret) {
36
- try {
37
- const [ivHex, tagHex, encryptedHex] = data.split(":");
38
- if (!ivHex || !tagHex || !encryptedHex) return null;
39
-
40
- const iv = Buffer.from(ivHex, "hex");
41
- const tag = Buffer.from(tagHex, "hex");
42
- const key = crypto.createHash("sha256").update(secret).digest();
43
- const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
44
- decipher.setAuthTag(tag);
45
-
46
- let decrypted = decipher.update(encryptedHex, "hex", "utf8");
47
- decrypted += decipher.final("utf8");
48
- return JSON.parse(decrypted);
49
- } catch (error) {
50
- return null;
51
- }
52
- }
53
-
54
- /**
55
- * Generates an encrypted JWT token.
56
- * @param {Object} payload - The data to store in the token.
57
- * @param {string} [secret=process.env.JWT_SECRET] - The secret key .
58
- * @param {string} [expiresIn="24h"] - Expiration time.
59
- * @returns {string} The generated token.
60
- */
61
- static generateToken(payload, secret = jwtSecret, expiresIn = "24h") {
62
- if (!secret)
63
- throw new Error("FATAL: JWT_SECRET environment variable is not defined.");
64
- const encrypted = this.encrypt(payload, secret);
65
- return jwt.sign({ data: encrypted }, secret, { expiresIn });
66
- }
67
-
68
- /**
69
- * Verifies and decrypts a JWT token.
70
- * @param {string} token - The token to verify.
71
- * @param {string} [secret=process.env.JWT_SECRET] - The secret key.
72
- * @returns {Object|null} The decoded and decrypted payload or null if invalid.
73
- */
74
- static verifyToken(token, secret = jwtSecret) {
75
- if (!secret)
76
- throw new Error("FATAL: JWT_SECRET environment variable is not defined.");
77
- try {
78
- const decoded = jwt.verify(token, secret);
79
- if (!decoded || !decoded.data) return null;
80
- return this.decrypt(decoded.data, secret);
81
- } catch (error) {
82
- return null;
83
- }
84
- }
85
-
86
- /**
87
- * Middleware to protect routes. Checks for token in Cookies or Authorization header.
88
- * @param {Object} [options={}] - Options for protection.
89
- * @param {string} [options.redirect=null] - URL to redirect if not authenticated.
90
- * @param {string} [options.key="user"] - Key to store the decoded token in the request.
91
- * @param {string} [options.cookieKey="auth"] - The cookie key to look for the token.
92
- * @returns {Function} Express middleware.
93
- * @example
94
- * router.get("/profile", Auth.protect(), (req, res) => { ... });
95
- * // Or with custom cookie key:
96
- * router.get("/admin", Auth.protect({ cookieKey: "admin_session" }), (req, res) => { ... });
97
- */
98
- static protect(options = {}) {
99
- const { redirect = null, key = "user", cookieKey = "auth" } = options;
100
-
101
- return async (req, res, next) => {
102
- const token =
103
- req.cookies?.[cookieKey] || req.headers.authorization?.split(" ")[1];
104
-
105
- const isContentTypeJson =
106
- req.headers["content-type"] === "application/json";
107
-
108
- if (!token) {
109
- if (redirect && !isContentTypeJson) return res.redirect(redirect);
110
- return isContentTypeJson
111
- ? res.status(401).json({ message: "Unauthorized" })
112
- : res.status(401).send();
113
- }
114
-
115
- const decoded = this.verifyToken(token);
116
- if (!decoded) {
117
- if (redirect && !isContentTypeJson) return res.redirect(redirect);
118
- return isContentTypeJson
119
- ? res.status(401).json({ message: "Unauthorized" })
120
- : res.status(401).send();
121
- }
122
-
123
- const redisClient = getRedisClient();
124
- if (redisClient && decoded._sessionId) {
125
- try {
126
- const sessionData = await redisClient.get(
127
- `session:${decoded._sessionId}`,
128
- );
129
- if (!sessionData) {
130
- if (redirect && !isContentTypeJson) return res.redirect(redirect);
131
- return isContentTypeJson
132
- ? res.status(401).json({ message: "Unauthorized" })
133
- : res.status(401).send();
134
- }
135
- req[key || "user"] = JSON.parse(sessionData);
136
- return next();
137
- } catch (err) {
138
- console.error(
139
- "[AUTH ERROR] Failed to get session data from Redis:",
140
- err.message,
141
- );
142
- if (redirect && !isContentTypeJson) return res.redirect(redirect);
143
- return isContentTypeJson
144
- ? res.status(401).json({ message: "Unauthorized" })
145
- : res.status(401).send();
146
- }
147
- }
148
-
149
- req[key || "user"] = decoded;
150
- next();
151
- };
152
- }
153
-
154
- /**
155
- * Logs in a user by setting an authentication cookie.
156
- * @param {import('express').Response} res - The response object.
157
- * @param {Object} data - The data to store in the token.
158
- * @param {string} [key="auth"] - The key for the cookie.
159
- * @param {Object} [options={}] - Options for the cookie and token.
160
- * @param {string} [options.expiresIn="24h"] - Token expiration (e.g., "1h", "7d").
161
- * @param {import('express').CookieOptions} [options.cookie] - Express cookie options.
162
- * @returns {Promise<string>} The generated token.
163
- * @example
164
- * await Auth.login(res, { id: 1, name: "Admin" });
165
- */
166
- static async login(res, data, key = "auth", options = {}) {
167
- const { expiresIn = "24h", cookie = {} } = options;
168
- const sessionId = crypto.randomUUID();
169
- const tokenPayload = { ...data, _sessionId: sessionId };
170
-
171
- const token = this.generateToken(tokenPayload, jwtSecret, expiresIn);
172
-
173
- const defaultCookieOptions = {
174
- maxAge: 24 * 60 * 60 * 1000,
175
- httpOnly: true,
176
- secure: production,
177
- sameSite: "strict",
178
- path: "/",
179
- };
180
-
181
- const finalCookieOptions = { ...defaultCookieOptions, ...cookie };
182
-
183
- const redisClient = getRedisClient();
184
- if (redisClient) {
185
- try {
186
- let ttl = 86400;
187
- if (typeof expiresIn === "string") {
188
- const match = expiresIn.match(/^(\d+)([smhd])$/);
189
- if (match) {
190
- const val = parseInt(match[1]);
191
- const unit = match[2];
192
- if (unit === "s") ttl = val;
193
- else if (unit === "m") ttl = val * 60;
194
- else if (unit === "h") ttl = val * 3600;
195
- else if (unit === "d") ttl = val * 86400;
196
- }
197
- } else if (typeof expiresIn === "number") {
198
- ttl = expiresIn;
199
- }
200
- await redisClient.set(`session:${sessionId}`, JSON.stringify(data), {
201
- EX: ttl,
202
- });
203
- } catch (err) {
204
- console.error(
205
- "[AUTH ERROR] Failed to store session in Redis:",
206
- err.message,
207
- );
208
- }
209
- }
210
-
211
- res.cookie(key, token, finalCookieOptions);
212
- return token;
213
- }
214
-
215
- /**
216
- * Logs out a user by clearing the authentication cookie.
217
- * @param {import('express').Response} res - The response object.
218
- * @param {string} [key="auth"] - The key for the cookie.
219
- * @param {import('express').CookieOptions} [options={}] - Options for clearing the cookie.
220
- * @param {import('express').Request} [req=null] - The request object.
221
- * @returns {Promise<boolean>} True if the cookie was cleared successfully.
222
- * @example
223
- * await Auth.logout(res);
224
- */
225
- static async logout(res, key = "auth", options = {}, req = null) {
226
- const defaultOptions = {
227
- path: "/",
228
- };
229
-
230
- if (req) {
231
- const token =
232
- req.cookies?.[key] || req.headers.authorization?.split(" ")[1];
233
- if (token) {
234
- const decoded = this.verifyToken(token);
235
- if (decoded && decoded._sessionId) {
236
- const redisClient = getRedisClient();
237
- if (redisClient) {
238
- try {
239
- await redisClient.del(`session:${decoded._sessionId}`);
240
- } catch (err) {
241
- console.error(
242
- "[AUTH ERROR] Failed to delete session from Redis:",
243
- err.message,
244
- );
245
- }
246
- }
247
- }
248
- }
249
- }
250
-
251
- res.clearCookie(key, { ...defaultOptions, ...options });
252
- return true;
253
- }
254
- }
255
-
256
- export default Auth;
1
+ import jwt from "jsonwebtoken";
2
+ import crypto from "node:crypto";
3
+ import Config from "./config.js";
4
+ import { getRedisClient } from "./cache.js";
5
+
6
+ const propsConfig = Config.props();
7
+ const jwtSecret = propsConfig.jwtSecret;
8
+ const production = !propsConfig.debug;
9
+
10
+ const aesKey = crypto.createHash("sha256").update(jwtSecret || "default").digest();
11
+
12
+ /**
13
+ * Auth class to handle JWT generation, verification and protection with AES-256-GCM encryption.
14
+ */
15
+ class Auth {
16
+ /**
17
+ * Encrypts a payload using AES-256-GCM.
18
+ * @param {Object} payload - The data to encrypt.
19
+ * @param {string} secret - The secret key for encryption.
20
+ * @returns {string} The encrypted string in format iv:tag:encrypted.
21
+ */
22
+ static encrypt(payload, secret) {
23
+ const iv = crypto.randomBytes(12);
24
+ const key = secret === jwtSecret ? aesKey : crypto.createHash("sha256").update(secret).digest();
25
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
26
+ let encrypted = cipher.update(JSON.stringify(payload), "utf8", "hex");
27
+ encrypted += cipher.final("hex");
28
+ const tag = cipher.getAuthTag().toString("hex");
29
+ return `${iv.toString("hex")}:${tag}:${encrypted}`;
30
+ }
31
+
32
+ /**
33
+ * Decrypts a payload using AES-256-GCM.
34
+ * @param {string} data - The encrypted string in format iv:tag:encrypted.
35
+ * @param {string} secret - The secret key for decryption.
36
+ * @returns {Object|null} The decrypted object or null if failed.
37
+ */
38
+ static decrypt(data, secret) {
39
+ try {
40
+ const [ivHex, tagHex, encryptedHex] = data.split(":");
41
+ if (!ivHex || !tagHex || !encryptedHex) return null;
42
+
43
+ const iv = Buffer.from(ivHex, "hex");
44
+ const tag = Buffer.from(tagHex, "hex");
45
+ const key = secret === jwtSecret ? aesKey : crypto.createHash("sha256").update(secret).digest();
46
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
47
+ decipher.setAuthTag(tag);
48
+
49
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
50
+ decrypted += decipher.final("utf8");
51
+ return JSON.parse(decrypted);
52
+ } catch (error) {
53
+ return null;
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Generates an encrypted JWT token.
59
+ * @param {Object} payload - The data to store in the token.
60
+ * @param {string} [secret=process.env.JWT_SECRET] - The secret key .
61
+ * @param {string} [expiresIn="24h"] - Expiration time.
62
+ * @returns {string} The generated token.
63
+ */
64
+ static generateToken(payload, secret = jwtSecret, expiresIn = "24h") {
65
+ if (!secret)
66
+ throw new Error("FATAL: JWT_SECRET environment variable is not defined.");
67
+ const encrypted = this.encrypt(payload, secret);
68
+ return jwt.sign({ data: encrypted }, secret, { expiresIn });
69
+ }
70
+
71
+ /**
72
+ * Verifies and decrypts a JWT token.
73
+ * @param {string} token - The token to verify.
74
+ * @param {string} [secret=process.env.JWT_SECRET] - The secret key.
75
+ * @returns {Object|null} The decoded and decrypted payload or null if invalid.
76
+ */
77
+ static verifyToken(token, secret = jwtSecret) {
78
+ if (!secret)
79
+ throw new Error("FATAL: JWT_SECRET environment variable is not defined.");
80
+ try {
81
+ const decoded = jwt.verify(token, secret);
82
+ if (!decoded || !decoded.data) return null;
83
+ return this.decrypt(decoded.data, secret);
84
+ } catch (error) {
85
+ return null;
86
+ }
87
+ }
88
+
89
+ /**
90
+ * Middleware to protect routes. Checks for token in Cookies or Authorization header.
91
+ * @param {Object} [options={}] - Options for protection.
92
+ * @param {string} [options.redirect=null] - URL to redirect if not authenticated.
93
+ * @param {string} [options.key="user"] - Key to store the decoded token in the request.
94
+ * @param {string} [options.cookieKey="auth"] - The cookie key to look for the token.
95
+ * @returns {Function} Express middleware.
96
+ * @example
97
+ * router.get("/profile", Auth.protect(), (req, res) => { ... });
98
+ * // Or with custom cookie key:
99
+ * router.get("/admin", Auth.protect({ cookieKey: "admin_session" }), (req, res) => { ... });
100
+ */
101
+ static protect(options = {}) {
102
+ const { redirect = null, key = "user", cookieKey = "auth" } = options;
103
+
104
+ return async (req, res, next) => {
105
+ const token =
106
+ req.cookies?.[cookieKey] || req.headers.authorization?.split(" ")[1];
107
+
108
+ const expectsJson =
109
+ req.xhr ||
110
+ req.headers.accept?.includes("application/json") ||
111
+ req.path.startsWith("/api");
112
+
113
+ if (!token) {
114
+ if (redirect && !expectsJson) return res.redirect(redirect);
115
+ return expectsJson
116
+ ? res.status(401).json({ message: "Unauthorized" })
117
+ : res.status(401).send();
118
+ }
119
+
120
+ const decoded = this.verifyToken(token);
121
+ if (!decoded) {
122
+ if (redirect && !expectsJson) return res.redirect(redirect);
123
+ return expectsJson
124
+ ? res.status(401).json({ message: "Unauthorized" })
125
+ : res.status(401).send();
126
+ }
127
+
128
+ const redisClient = getRedisClient();
129
+ if (redisClient && decoded._sessionId) {
130
+ try {
131
+ const sessionData = await redisClient.get(
132
+ `session:${decoded._sessionId}`,
133
+ );
134
+ if (!sessionData) {
135
+ if (redirect && !expectsJson) return res.redirect(redirect);
136
+ return expectsJson
137
+ ? res.status(401).json({ message: "Unauthorized" })
138
+ : res.status(401).send();
139
+ }
140
+ req[key || "user"] = JSON.parse(sessionData);
141
+ return next();
142
+ } catch (err) {
143
+ console.error(
144
+ "[AUTH ERROR] Failed to get session data from Redis:",
145
+ err.message,
146
+ );
147
+ if (redirect && !expectsJson) return res.redirect(redirect);
148
+ return expectsJson
149
+ ? res.status(401).json({ message: "Unauthorized" })
150
+ : res.status(401).send();
151
+ }
152
+ }
153
+
154
+ req[key || "user"] = decoded;
155
+ next();
156
+ };
157
+ }
158
+
159
+ /**
160
+ * Logs in a user by setting an authentication cookie.
161
+ * @param {import('express').Response} res - The response object.
162
+ * @param {Object} data - The data to store in the token.
163
+ * @param {string} [key="auth"] - The key for the cookie.
164
+ * @param {Object} [options={}] - Options for the cookie and token.
165
+ * @param {string} [options.expiresIn="24h"] - Token expiration (e.g., "1h", "7d").
166
+ * @param {import('express').CookieOptions} [options.cookie] - Express cookie options.
167
+ * @returns {Promise<string>} The generated token.
168
+ * @example
169
+ * await Auth.login(res, { id: 1, name: "Admin" });
170
+ */
171
+ static async login(res, data, key = "auth", options = {}) {
172
+ const { expiresIn = "24h", cookie = {} } = options;
173
+ const sessionId = crypto.randomUUID();
174
+ const tokenPayload = { ...data, _sessionId: sessionId };
175
+
176
+ const token = this.generateToken(tokenPayload, jwtSecret, expiresIn);
177
+
178
+ let ttl = 86400; // default 24h
179
+ if (typeof expiresIn === "string") {
180
+ const match = expiresIn.match(/^(\d+)([smhd])$/);
181
+ if (match) {
182
+ const val = parseInt(match[1]);
183
+ const unit = match[2];
184
+ if (unit === "s") ttl = val;
185
+ else if (unit === "m") ttl = val * 60;
186
+ else if (unit === "h") ttl = val * 3600;
187
+ else if (unit === "d") ttl = val * 86400;
188
+ }
189
+ } else if (typeof expiresIn === "number") {
190
+ ttl = expiresIn;
191
+ }
192
+
193
+ const defaultCookieOptions = {
194
+ maxAge: ttl * 1000,
195
+ httpOnly: true,
196
+ secure: production,
197
+ sameSite: "strict",
198
+ path: "/",
199
+ };
200
+
201
+ const finalCookieOptions = { ...defaultCookieOptions, ...cookie };
202
+
203
+ const redisClient = getRedisClient();
204
+ if (redisClient) {
205
+ try {
206
+ await redisClient.set(`session:${sessionId}`, JSON.stringify(data), {
207
+ EX: ttl,
208
+ });
209
+ } catch (err) {
210
+ console.error(
211
+ "[AUTH ERROR] Failed to store session in Redis:",
212
+ err.message,
213
+ );
214
+ }
215
+ }
216
+
217
+ res.cookie(key, token, finalCookieOptions);
218
+ return token;
219
+ }
220
+
221
+ /**
222
+ * Logs out a user by clearing the authentication cookie.
223
+ * @param {import('express').Response} res - The response object.
224
+ * @param {string} [key="auth"] - The key for the cookie.
225
+ * @param {import('express').CookieOptions} [options={}] - Options for clearing the cookie.
226
+ * @param {import('express').Request} [req=null] - The request object.
227
+ * @returns {Promise<boolean>} True if the cookie was cleared successfully.
228
+ * @example
229
+ * await Auth.logout(res);
230
+ */
231
+ static async logout(res, key = "auth", options = {}, req = null) {
232
+ const defaultOptions = {
233
+ path: "/",
234
+ };
235
+
236
+ if (req) {
237
+ const token =
238
+ req.cookies?.[key] || req.headers.authorization?.split(" ")[1];
239
+ if (token) {
240
+ const decoded = this.verifyToken(token);
241
+ if (decoded && decoded._sessionId) {
242
+ const redisClient = getRedisClient();
243
+ if (redisClient) {
244
+ try {
245
+ await redisClient.del(`session:${decoded._sessionId}`);
246
+ } catch (err) {
247
+ console.error(
248
+ "[AUTH ERROR] Failed to delete session from Redis:",
249
+ err.message,
250
+ );
251
+ }
252
+ }
253
+ }
254
+ }
255
+ }
256
+
257
+ res.clearCookie(key, { ...defaultOptions, ...options });
258
+ return true;
259
+ }
260
+ }
261
+
262
+ export default Auth;