@seip/blue-bird 0.4.4 → 0.4.5

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andres Paiva
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.
@@ -1,31 +1,31 @@
1
- import Router from "@seip/blue-bird/core/router.js";
2
- import Validator from "@seip/blue-bird/core/validate.js";
3
-
4
- const routerApiExample = new Router("/api");
5
-
6
- routerApiExample.get("/users", (req, res) => {
7
- const users = [
8
- {
9
- name: "John Doe",
10
- email: "john.doe@example.com",
11
- },
12
- {
13
- name: "Jane Doe2",
14
- email: "jane.doe2@example.com",
15
- },
16
- ];
17
- res.json(users);
18
- });
19
-
20
- const loginSchema = {
21
- email: { required: true, email: true },
22
- password: { required: true, min: 6 },
23
- };
24
-
25
- const loginValidator = new Validator(loginSchema);
26
-
27
- routerApiExample.post("/login", loginValidator.middleware(), (req, res) => {
28
- res.json({ message: "Login successful", body: req.body });
29
- });
30
-
31
- export default routerApiExample;
1
+ import Router from "@seip/blue-bird/core/router.js";
2
+ import Validator from "@seip/blue-bird/core/validate.js";
3
+
4
+ const routerApiExample = new Router("/api");
5
+
6
+ routerApiExample.get("/users", (req, res) => {
7
+ const users = [
8
+ {
9
+ name: "John Doe",
10
+ email: "john.doe@example.com",
11
+ },
12
+ {
13
+ name: "Jane Doe2",
14
+ email: "jane.doe2@example.com",
15
+ },
16
+ ];
17
+ res.json(users);
18
+ });
19
+
20
+ const loginSchema = {
21
+ email: { required: true, email: true },
22
+ password: { required: true, min: 6 },
23
+ };
24
+
25
+ const loginValidator = new Validator(loginSchema);
26
+
27
+ routerApiExample.post("/login", loginValidator.middleware(), (req, res) => {
28
+ res.json({ message: "Login successful", body: req.body });
29
+ });
30
+
31
+ export default routerApiExample;
package/core/auth.js CHANGED
@@ -1,45 +1,82 @@
1
1
  import jwt from "jsonwebtoken";
2
+ import crypto from "node:crypto";
2
3
 
3
4
  /**
4
- * Auth class to handle JWT generation, verification and protection.
5
+ * Auth class to handle JWT generation, verification and protection with AES-256-GCM encryption.
5
6
  */
6
7
  class Auth {
7
8
  /**
8
- * Generates a JWT token.
9
+ * Encrypts a payload using AES-256-GCM.
10
+ * @param {Object} payload - The data to encrypt.
11
+ * @param {string} secret - The secret key for encryption.
12
+ * @returns {string} The encrypted string in format iv:tag:encrypted.
13
+ */
14
+ static encrypt(payload, secret) {
15
+ const iv = crypto.randomBytes(12);
16
+ const key = crypto.createHash("sha256").update(secret).digest();
17
+ const cipher = crypto.createCipheriv("aes-256-gcm", key, iv);
18
+ let encrypted = cipher.update(JSON.stringify(payload), "utf8", "hex");
19
+ encrypted += cipher.final("hex");
20
+ const tag = cipher.getAuthTag().toString("hex");
21
+ return `${iv.toString("hex")}:${tag}:${encrypted}`;
22
+ }
23
+
24
+ /**
25
+ * Decrypts a payload using AES-256-GCM.
26
+ * @param {string} data - The encrypted string in format iv:tag:encrypted.
27
+ * @param {string} secret - The secret key for decryption.
28
+ * @returns {Object|null} The decrypted object or null if failed.
29
+ */
30
+ static decrypt(data, secret) {
31
+ try {
32
+ const [ivHex, tagHex, encryptedHex] = data.split(":");
33
+ if (!ivHex || !tagHex || !encryptedHex) return null;
34
+
35
+ const iv = Buffer.from(ivHex, "hex");
36
+ const tag = Buffer.from(tagHex, "hex");
37
+ const key = crypto.createHash("sha256").update(secret).digest();
38
+ const decipher = crypto.createDecipheriv("aes-256-gcm", key, iv);
39
+ decipher.setAuthTag(tag);
40
+
41
+ let decrypted = decipher.update(encryptedHex, "hex", "utf8");
42
+ decrypted += decipher.final("utf8");
43
+ return JSON.parse(decrypted);
44
+ } catch (error) {
45
+ return null;
46
+ }
47
+ }
48
+
49
+ /**
50
+ * Generates an encrypted JWT token.
9
51
  * @param {Object} payload - The data to store in the token.
10
52
  * @param {string} [secret=process.env.JWT_SECRET] - The secret key.
11
- * @param {string|number} [expiresIn='24h'] - Expiration time.
53
+ * @param {string} [expiresIn="24h"] - Expiration time.
12
54
  * @returns {string} The generated token.
13
- * @example
14
- * const token = Auth.generateToken({ id: 1 });
15
- * console.log(token);
16
- *
17
55
  */
18
56
  static generateToken(
19
57
  payload,
20
58
  secret = process.env.JWT_SECRET,
21
- expiresIn = "24h",
59
+ expiresIn = "24h"
22
60
  ) {
23
61
  if (!secret)
24
62
  throw new Error("FATAL: JWT_SECRET environment variable is not defined.");
25
- return jwt.sign(payload, secret, { expiresIn });
63
+ const encrypted = this.encrypt(payload, secret);
64
+ return jwt.sign({ data: encrypted }, secret, { expiresIn });
26
65
  }
27
66
 
28
67
  /**
29
- * Verifies a JWT token.
68
+ * Verifies and decrypts a JWT token.
30
69
  * @param {string} token - The token to verify.
31
70
  * @param {string} [secret=process.env.JWT_SECRET] - The secret key.
32
- * @returns {Object|null} The decoded payload or null if invalid.
33
- * @example
34
- * const token = Auth.generateToken({ id: 1 });
35
- * const decoded = Auth.verifyToken(token);
36
- * console.log(decoded);
71
+ * @returns {Object|null} The decoded and decrypted payload or null if invalid.
37
72
  */
38
73
  static verifyToken(token, secret = process.env.JWT_SECRET) {
39
74
  if (!secret)
40
75
  throw new Error("FATAL: JWT_SECRET environment variable is not defined.");
41
76
  try {
42
- return jwt.verify(token, secret);
77
+ const decoded = jwt.verify(token, secret);
78
+ if (!decoded || !decoded.data) return null;
79
+ return this.decrypt(decoded.data, secret);
43
80
  } catch (error) {
44
81
  return null;
45
82
  }
@@ -48,33 +85,27 @@ class Auth {
48
85
  /**
49
86
  * Middleware to protect routes. Checks for token in Cookies or Authorization header.
50
87
  * @param {Object} options - Options for protection.
51
- * @param {string} [options.redirect=null] - URL to redirect if not authenticated (for web routes).
88
+ * @param {string} [options.redirect=null] - URL to redirect if not authenticated.
52
89
  * @param {string} [options.key="user"] - Key to store the decoded token in the request.
53
90
  * @returns {Function} Express middleware.
54
- * @example
55
- * app.use(Auth.protect({ redirect: "/login" ,key:"user"}));
56
91
  */
57
- static protect(options = {}) {
58
- const { redirect = null, key = "user" } = options;
59
-
92
+ static protect(options = { redirect: null, key: "user" }) {
60
93
  return (req, res, next) => {
61
94
  const token =
62
- req.cookies?.token || req.headers.authorization?.split(" ")[1];
95
+ req.cookies?.auth || req.headers.authorization?.split(" ")[1];
63
96
 
64
97
  if (!token) {
65
- if (redirect) return res.redirect(redirect);
66
- return res
67
- .status(401)
68
- .json({ message: "Unauthorized: No token provided" });
98
+ if (options.redirect) return res.redirect(options.redirect);
99
+ return res.status(401).json({ message: "Unauthorized" });
69
100
  }
70
101
 
71
102
  const decoded = this.verifyToken(token);
72
103
  if (!decoded) {
73
- if (redirect) return res.redirect(redirect);
74
- return res.status(401).json({ message: "Unauthorized: Invalid token" });
104
+ if (options.redirect) return res.redirect(options.redirect);
105
+ return res.status(401).json({ message: "Unauthorized" });
75
106
  }
76
107
 
77
- req[key] = decoded;
108
+ req[options.key || "user"] = decoded;
78
109
  next();
79
110
  };
80
111
  }
@@ -0,0 +1,1037 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import chalk from "chalk";
4
+ import { execSync } from "node:child_process";
5
+ import { fileURLToPath } from "node:url";
6
+
7
+ /**
8
+ * Scaffolds an authentication system with Raw DB Queries, JWT, React Frontend, and i18n support.
9
+ */
10
+ class ScaffoldingAuth {
11
+ constructor() {
12
+ this.appDir = process.cwd();
13
+ this.backendDir = path.join(this.appDir, "backend");
14
+ this.frontendDir = path.join(this.appDir, "frontend");
15
+ }
16
+
17
+ async run() {
18
+ console.log(chalk.cyan("Starting Auth Scaffolding initialization..."));
19
+
20
+ try {
21
+ this.setupDatabaseAndServices();
22
+ this.createBackendRoutes();
23
+ this.createFrontendComponents();
24
+ this.modifyEntryFiles();
25
+
26
+ console.log(chalk.blue("\nAuth Scaffolding completed successfully!"));
27
+ console.log(chalk.white("You can use the native database connection provided in backend/databases/connection.js"));
28
+ console.log(chalk.white("OR we recommend using an ORM like Prisma. If you migrate to an ORM, adapt backend/databases/services/auth.service.js to use it."));
29
+ console.log(chalk.white("Update your App.jsx to use the newly created React components."));
30
+ console.log(chalk.yellow("Running setup script to execute database migrations..."));
31
+ try {
32
+ execSync('node --env-file=.env backend/databases/setup_tables.js', { stdio: "inherit", cwd: this.appDir });
33
+ console.log(chalk.green("✓ Database migrations success. Users and LoginHistory tables created."));
34
+ } catch (e) {
35
+ console.log(chalk.red("Failed to execute setup_tables.js. You may need to run it manually."));
36
+ console.log(e);
37
+ }
38
+ } catch (error) {
39
+ console.error(chalk.red("Error during Scaffolding Auth initialization:"), error.message);
40
+ }
41
+ }
42
+
43
+ setupDatabaseAndServices() {
44
+ const dbDir = path.join(this.backendDir, "databases");
45
+ const servicesDir = path.join(dbDir, "services");
46
+ if (!fs.existsSync(dbDir)) fs.mkdirSync(dbDir, { recursive: true });
47
+ if (!fs.existsSync(servicesDir)) fs.mkdirSync(servicesDir, { recursive: true });
48
+
49
+ const envPath = path.join(this.appDir, ".env");
50
+ let dbUrl = "";
51
+ let dialect = "sqlite";
52
+ let pkg = "sqlite3";
53
+
54
+ if (fs.existsSync(envPath)) {
55
+ const envContent = fs.readFileSync(envPath, "utf-8");
56
+ const dbUrlMatch = envContent.match(/DATABASE_URL="?([^"\n]+)"?/);
57
+ if (dbUrlMatch) {
58
+ dbUrl = dbUrlMatch[1];
59
+ if (dbUrl.startsWith("mysql")) { dialect = "mysql"; pkg = "mysql2"; }
60
+ if (dbUrl.startsWith("postgres")) { dialect = "postgres"; pkg = "pg"; }
61
+ }
62
+ }
63
+
64
+ console.log(chalk.yellow(`Detected database dialect: ${dialect}. Installing ${pkg}...`));
65
+ try {
66
+ execSync(`npm install ${pkg}`, { stdio: "inherit", cwd: this.appDir });
67
+ execSync(`npm install bcrypt`, { stdio: "inherit", cwd: this.appDir });
68
+ } catch (e) {
69
+ console.log(chalk.red(`Failed to install ${pkg}. Please install it manually.`));
70
+ }
71
+
72
+ this.generateConnectionFile(dbDir, dialect);
73
+ this.generateSetupTables(dbDir, dialect);
74
+ this.generateAuthService(servicesDir, dialect);
75
+
76
+
77
+ console.log(chalk.green("✓ Database connection, setup script, and services generated."));
78
+
79
+
80
+ }
81
+
82
+ generateConnectionFile(dbDir, dialect) {
83
+ let content = `import Config from "@seip/blue-bird/core/config.js";\n\nconst props = Config.props();\nconst dbUrl = process.env.DATABASE_URL || "file:./dev.db";\n`;
84
+
85
+ if (dialect === "sqlite") {
86
+ content += `import sqlite3 from 'sqlite3';\n
87
+ class DatabaseConnection {
88
+ constructor() {
89
+ this.db = null;
90
+ this.dbPath = dbUrl.replace("file:", "");
91
+ }
92
+
93
+ async connect(retries = 5, delay = 2000) {
94
+ if (this.db) return this.db;
95
+ for (let i = 0; i < retries; i++) {
96
+ try {
97
+ return await new Promise((resolve, reject) => {
98
+ this.db = new sqlite3.Database(this.dbPath, (err) => {
99
+ if (err) reject(err);
100
+ else resolve(this.db);
101
+ });
102
+ });
103
+ } catch (err) {
104
+ if (props.debug) console.log(\`[DEBUG] Database connection failed. Retrying in \${delay / 1000}s... (\${i + 1}/\${retries})\`);
105
+ await new Promise(res => setTimeout(res, delay));
106
+ }
107
+ }
108
+ const errorMsg = "Database connection failed after maximum retries.";
109
+ if (props.debug) console.error("[ERROR]", errorMsg);
110
+ throw new Error(errorMsg);
111
+ }
112
+
113
+ async query(sql, params = []) {
114
+ await this.connect();
115
+ return new Promise((resolve, reject) => {
116
+ if (sql.trim().toLowerCase().startsWith('select')) {
117
+ this.db.all(sql, params, (err, rows) => {
118
+ if (err) reject(err);
119
+ else resolve(rows);
120
+ });
121
+ } else {
122
+ this.db.run(sql, params, function(err) {
123
+ if (err) reject(err);
124
+ else resolve({ id: this.lastID, changes: this.changes });
125
+ });
126
+ }
127
+ });
128
+ }
129
+
130
+ async queryOne(sql, params = []) {
131
+ await this.connect();
132
+ return new Promise((resolve, reject) => {
133
+ this.db.get(sql, params, (err, row) => {
134
+ if (err) reject(err);
135
+ else resolve(row);
136
+ });
137
+ });
138
+ }
139
+ }
140
+ `;
141
+ } else if (dialect === "mysql") {
142
+ content += `import mysql from 'mysql2/promise';\n
143
+ class DatabaseConnection {
144
+ constructor() {
145
+ this.pool = null;
146
+ }
147
+
148
+ async connect(retries = 5, delay = 2000) {
149
+ if (this.pool) return this.pool;
150
+ for (let i = 0; i < retries; i++) {
151
+ try {
152
+ this.pool = mysql.createPool(dbUrl);
153
+ await this.pool.query('SELECT 1');
154
+ return this.pool;
155
+ } catch (err) {
156
+ if (props.debug) console.log(\`[DEBUG] Database connection failed. Retrying in \${delay / 1000}s... (\${i + 1}/\${retries})\`);
157
+ await new Promise(res => setTimeout(res, delay));
158
+ }
159
+ }
160
+ const errorMsg = "Database connection failed after maximum retries.";
161
+ if (props.debug) console.error("[ERROR]", errorMsg);
162
+ throw new Error(errorMsg);
163
+ }
164
+
165
+ async query(sql, params = []) {
166
+ await this.connect();
167
+ const [rows] = await this.pool.query(sql, params);
168
+ if (sql.trim().toLowerCase().startsWith('insert') || sql.trim().toLowerCase().startsWith('update') || sql.trim().toLowerCase().startsWith('delete')) {
169
+ return { changes: rows.affectedRows, id: rows.insertId };
170
+ }
171
+ return rows;
172
+ }
173
+
174
+ async queryOne(sql, params = []) {
175
+ await this.connect();
176
+ const [rows] = await this.pool.query(sql, params);
177
+ return rows.length ? rows[0] : null;
178
+ }
179
+ }
180
+ `;
181
+ } else if (dialect === "postgres") {
182
+ content += `import pkg from 'pg';\nconst { Pool } = pkg;\n
183
+ class DatabaseConnection {
184
+ constructor() {
185
+ this.pool = null;
186
+ }
187
+
188
+ async connect(retries = 5, delay = 2000) {
189
+ if (this.pool) return this.pool;
190
+ for (let i = 0; i < retries; i++) {
191
+ try {
192
+ this.pool = new Pool({ connectionString: dbUrl });
193
+ await this.pool.query('SELECT 1');
194
+ return this.pool;
195
+ } catch (err) {
196
+ if (props.debug) console.log(\`[DEBUG] Database connection failed. Retrying in \${delay / 1000}s... (\${i + 1}/\${retries})\`);
197
+ await new Promise(res => setTimeout(res, delay));
198
+ }
199
+ }
200
+ const errorMsg = "Database connection failed after maximum retries.";
201
+ if (props.debug) console.error("[ERROR]", errorMsg);
202
+ throw new Error(errorMsg);
203
+ }
204
+
205
+ async query(sql, params = []) {
206
+ await this.connect();
207
+ const res = await this.pool.query(sql, params);
208
+ if (sql.trim().toLowerCase().startsWith('insert') || sql.trim().toLowerCase().startsWith('update') || sql.trim().toLowerCase().startsWith('delete')) {
209
+ return { changes: res.rowCount };
210
+ }
211
+ return res.rows;
212
+ }
213
+
214
+ async queryOne(sql, params = []) {
215
+ await this.connect();
216
+ const res = await this.pool.query(sql, params);
217
+ return res.rows.length ? res.rows[0] : null;
218
+ }
219
+ }
220
+ `;
221
+ }
222
+
223
+ content += `\nconst db = new DatabaseConnection();\nexport default db;\n`;
224
+ fs.writeFileSync(path.join(dbDir, "connection.js"), content, "utf-8");
225
+ }
226
+
227
+ generateSetupTables(dbDir, dialect) {
228
+ let content = `import db from './connection.js';\n\n`;
229
+
230
+ if (dialect === "mysql" || dialect === "postgres") {
231
+ content += `const dbUrl = process.env.DATABASE_URL;\n\nasync function createDatabaseIfNotExists() {\n`;
232
+ if (dialect === "mysql") {
233
+ content += ` try {
234
+ const mysql = await import('mysql2/promise');
235
+ const createConnection = mysql.createConnection || (mysql.default && mysql.default.createConnection);
236
+ const url = new URL(dbUrl);
237
+ const dbName = url.pathname.replace('/', '');
238
+ const connectionParams = {
239
+ host: url.hostname,
240
+ user: decodeURIComponent(url.username),
241
+ password: decodeURIComponent(url.password),
242
+ port: url.port ? Number(url.port) : 3306
243
+ };
244
+ const connection = await createConnection(connectionParams);
245
+ await connection.query(\`CREATE DATABASE IF NOT EXISTS \\\`\${dbName}\\\`;\`);
246
+ await connection.end();
247
+ console.log(\`Database '\${dbName}' checked/created successfully.\`);
248
+ } catch (err) {
249
+ console.error("Failed to create database automatically:", err.message);
250
+ }
251
+ `;
252
+ } else if (dialect === "postgres") {
253
+ content += ` try {
254
+ const pkg = await import('pg');
255
+ const Client = pkg.Client || (pkg.default && pkg.default.Client);
256
+ const url = new URL(dbUrl);
257
+ const dbName = url.pathname.replace('/', '');
258
+ const connectionString = \`postgres://\${url.username}:\${url.password}@\${url.hostname}:\${url.port ? url.port : 5432}/postgres\`;
259
+ const client = new Client({ connectionString });
260
+ await client.connect();
261
+
262
+ const res = await client.query('SELECT 1 FROM pg_database WHERE datname = $1', [dbName]);
263
+ if (res.rowCount === 0) {
264
+ await client.query(\`CREATE DATABASE "\${dbName}"\`);
265
+ console.log(\`Database '\${dbName}' created successfully.\`);
266
+ } else {
267
+ console.log(\`Database '\${dbName}' already exists.\`);
268
+ }
269
+ await client.end();
270
+ } catch (err) {
271
+ console.error("Failed to create database automatically:", err.message);
272
+ }
273
+ `;
274
+ }
275
+ content += `}\n\n`;
276
+ }
277
+
278
+ content += `async function setup() {\n`;
279
+
280
+ if (dialect === "mysql" || dialect === "postgres") {
281
+ content += ` await createDatabaseIfNotExists();\n`;
282
+ }
283
+
284
+ let usersTable = "";
285
+ let historyTable = "";
286
+
287
+ if (dialect === "sqlite") {
288
+ usersTable = `
289
+ await db.query(\`
290
+ CREATE TABLE IF NOT EXISTS User (
291
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
292
+ id_public TEXT UNIQUE NOT NULL,
293
+ name TEXT NOT NULL,
294
+ email TEXT UNIQUE NOT NULL,
295
+ is_active INTEGER DEFAULT 1,
296
+ password TEXT NOT NULL,
297
+ password_token TEXT,
298
+ remember_token TEXT,
299
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
300
+ )
301
+ \`);`;
302
+
303
+ historyTable = `
304
+ await db.query(\`
305
+ CREATE TABLE IF NOT EXISTS LoginHistory (
306
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
307
+ email TEXT NOT NULL,
308
+ ip_address TEXT NOT NULL,
309
+ success INTEGER NOT NULL,
310
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
311
+ )
312
+ \`);`;
313
+ } else if (dialect === "mysql") {
314
+ usersTable = `
315
+ await db.query(\`
316
+ CREATE TABLE IF NOT EXISTS User (
317
+ id INT AUTO_INCREMENT PRIMARY KEY,
318
+ id_public VARCHAR(36) UNIQUE NOT NULL,
319
+ name VARCHAR(255) NOT NULL,
320
+ email VARCHAR(255) UNIQUE NOT NULL,
321
+ is_active BOOLEAN DEFAULT TRUE,
322
+ password VARCHAR(255) NOT NULL,
323
+ password_token VARCHAR(255),
324
+ remember_token VARCHAR(255),
325
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
326
+ )
327
+ \`);`;
328
+
329
+ historyTable = `
330
+ await db.query(\`
331
+ CREATE TABLE IF NOT EXISTS LoginHistory (
332
+ id INT AUTO_INCREMENT PRIMARY KEY,
333
+ email VARCHAR(255) NOT NULL,
334
+ ip_address VARCHAR(45) NOT NULL,
335
+ success BOOLEAN NOT NULL,
336
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
337
+ )
338
+ \`);`;
339
+ } else if (dialect === "postgres") {
340
+ usersTable = `
341
+ await db.query(\`
342
+ CREATE TABLE IF NOT EXISTS "User" (
343
+ id SERIAL PRIMARY KEY,
344
+ id_public UUID UNIQUE NOT NULL,
345
+ name VARCHAR(255) NOT NULL,
346
+ email VARCHAR(255) UNIQUE NOT NULL,
347
+ is_active BOOLEAN DEFAULT TRUE,
348
+ password VARCHAR(255) NOT NULL,
349
+ password_token VARCHAR(255),
350
+ remember_token VARCHAR(255),
351
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
352
+ )
353
+ \`);`;
354
+
355
+ historyTable = `
356
+ await db.query(\`
357
+ CREATE TABLE IF NOT EXISTS "LoginHistory" (
358
+ id SERIAL PRIMARY KEY,
359
+ email VARCHAR(255) NOT NULL,
360
+ ip_address VARCHAR(45) NOT NULL,
361
+ success BOOLEAN NOT NULL,
362
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
363
+ )
364
+ \`);`;
365
+ }
366
+
367
+ content += `${usersTable}\n${historyTable}\n console.log("Tables created successfully.");\n process.exit(0);\n}\n\nsetup().catch(err => {\n console.error(err);\n process.exit(1);\n});\n`;
368
+ fs.writeFileSync(path.join(dbDir, "setup_tables.js"), content, "utf-8");
369
+ }
370
+
371
+ generateAuthService(servicesDir, dialect) {
372
+ let content = `import db from '../connection.js';
373
+ import crypto from 'node:crypto';
374
+ import { hash } from 'bcrypt';
375
+
376
+ export default class AuthService {
377
+ static async checkRateLimit(ip, email) {
378
+ const tenMinsAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString();
379
+ `;
380
+
381
+ if (dialect === "postgres") {
382
+ content += ` const sql = \`SELECT COUNT(*) as count FROM "LoginHistory" WHERE ip_address = $1 AND email = $2 AND success = false AND created_at >= $3\`;\n const result = await db.queryOne(sql, [ip, email, tenMinsAgo]);\n`;
383
+ } else {
384
+ content += ` const sql = \`SELECT COUNT(*) as count FROM LoginHistory WHERE ip_address = ? AND email = ? AND success = 0 AND created_at >= ?\`;\n const result = await db.queryOne(sql, [ip, email, tenMinsAgo]);\n`;
385
+ }
386
+
387
+ content += ` const attempts = result ? parseInt(result.count || result.COUNT) : 0;
388
+
389
+ if (attempts >= 15) throw new Error("Blocked for 10 minutes.");
390
+ if (attempts >= 10) throw new Error("Blocked for 5 minutes.");
391
+ if (attempts >= 8) throw new Error("Blocked for 3 minutes.");
392
+ if (attempts >= 5) throw new Error("Blocked for 1 minute.");
393
+ return true;
394
+ }
395
+
396
+ static async findUserByEmail(email, isActive = 1) {
397
+ `;
398
+ if (dialect === "postgres") {
399
+ content += ` return await db.queryOne('SELECT * FROM "User" WHERE is_active = $1 AND email = $2', [isActive, email]);\n`;
400
+ } else {
401
+ content += ` return await db.queryOne('SELECT * FROM User WHERE is_active = ? AND email = ?', [isActive, email]);\n`;
402
+ }
403
+
404
+ content += ` }
405
+
406
+ static async createLoginHistory(email, ip, success) {
407
+ `;
408
+ if (dialect === "postgres") {
409
+ content += ` await db.query('INSERT INTO "LoginHistory" (email, ip_address, success) VALUES ($1, $2, $3)', [email, ip, success]);\n`;
410
+ } else {
411
+ content += ` const successVal = success ? 1 : 0;\n await db.query('INSERT INTO LoginHistory (email, ip_address, success) VALUES (?, ?, ?)', [email, ip, successVal]);\n`;
412
+ }
413
+
414
+ content += ` }
415
+
416
+ static async createUser(name, email, password) {
417
+ const id_public = crypto.randomUUID();
418
+ const passwordHash= await hash(password, 10);
419
+ `;
420
+
421
+ if (dialect === "postgres") {
422
+ content += ` await db.query('INSERT INTO "User" (id_public, name, email, password, is_active) VALUES ($1, $2, $3, $4, $5)', [id_public, name, email, passwordHash, true]);\n`;
423
+ } else {
424
+ content += ` await db.query('INSERT INTO User (id_public, name, email, password, is_active) VALUES (?, ?, ?, ?, ?)', [id_public, name, email, passwordHash, true]);\n`;
425
+ }
426
+
427
+ content += ` return { id_public, name, email };
428
+ }
429
+
430
+ static async setPasswordToken(userId, token) {
431
+ `;
432
+ if (dialect === "postgres") {
433
+ content += ` await db.query('UPDATE "User" SET password_token = $1 WHERE id = $2', [token, userId]);\n`;
434
+ } else {
435
+ content += ` await db.query('UPDATE User SET password_token = ? WHERE id = ?', [token, userId]);\n`;
436
+ }
437
+
438
+ content += ` }
439
+
440
+ static async findUserByPasswordToken(token, isActive = 1) {
441
+ `;
442
+ if (dialect === "postgres") {
443
+ content += ` return await db.queryOne('SELECT * FROM "User" WHERE password_token = $1 AND is_active = $2', [token, isActive]);\n`;
444
+ } else {
445
+ content += ` return await db.queryOne('SELECT * FROM User WHERE password_token = ? AND is_active = ?', [token, isActive]);\n`;
446
+ }
447
+
448
+ content += ` }
449
+
450
+ static async updatePassword(userId, newPassword) {
451
+ const newPasswordHash = await hash(newPassword, 10);
452
+ `;
453
+ if (dialect === "postgres") {
454
+ content += ` return await db.query('UPDATE "User" SET password = $1, password_token = NULL WHERE id = $2', [newPasswordHash, userId]);\n`;
455
+ } else {
456
+ content += ` return await db.query('UPDATE User SET password = ?, password_token = NULL WHERE id = ?', [newPasswordHash, userId]);\n`;
457
+ }
458
+
459
+ content += ` }
460
+ }
461
+ `;
462
+ fs.writeFileSync(path.join(servicesDir, "auth.service.js"), content, "utf-8");
463
+ }
464
+
465
+ createBackendRoutes() {
466
+ const routesDir = path.join(this.backendDir, "routes");
467
+ if (!fs.existsSync(routesDir)) fs.mkdirSync(routesDir, { recursive: true });
468
+
469
+ const authFile = path.join(routesDir, "auth.js");
470
+ const authenticatedFile = path.join(routesDir, "authenticated.js");
471
+
472
+ const authContent = `import Router from "@seip/blue-bird/core/router.js";
473
+ import Validator from "@seip/blue-bird/core/validate.js";
474
+ import Auth from "@seip/blue-bird/core/auth.js";
475
+ import Config from "@seip/blue-bird/core/config.js";
476
+ import crypto from "node:crypto";
477
+ import AuthService from "../databases/services/auth.service.js";
478
+ import { compare } from "bcrypt";
479
+ import Template from "@seip/blue-bird/core/template.js";
480
+
481
+ const routerAuth = new Router("/auth");
482
+ const props = Config.props();
483
+
484
+ routerAuth.post("/login", new Validator({ email: { required: true, email: true }, password: { required: true } }).middleware(), async (req, res) => {
485
+ try {
486
+ const { email, password } = req.body;
487
+ const ip = req.ip;
488
+
489
+ await AuthService.checkRateLimit(ip, email);
490
+
491
+ const user = await AuthService.findUserByEmail(email, 1);
492
+
493
+ if (!user) {
494
+ await AuthService.createLoginHistory(email, ip, false);
495
+ const deletedAccount = await AuthService.findUserByEmail(email, 0);
496
+ if (deletedAccount) return res.status(401).json({ message: "Your account is deleted", deleted_account_error: true });
497
+ return res.status(401).json({ message: "Invalid credentials" });
498
+ }
499
+ const passwordCompare = await compare(password, user.password);
500
+
501
+ if (!passwordCompare) {
502
+ await AuthService.createLoginHistory(email, ip, false);
503
+ return res.status(401).json({ message: "Invalid credentials" });
504
+ }
505
+ await AuthService.createLoginHistory(email, ip, true);
506
+ await AuthService.setPasswordToken(user.id, null);
507
+ const token = Auth.generateToken({ id: user.id_public, email: user.email });
508
+ const secure = props.debug == false ? true : false;
509
+ res.cookie("token", token, { httpOnly: true, secure: secure, sameSite: "strict", maxAge: 24 * 60 * 60 * 1000 });
510
+ return res.json({ user: { id: user.id_public, name: user.name, email: user.email } });
511
+ } catch (error) {
512
+ console.log(error)
513
+ return res.status(429).json({ message: props.debug ? error.message : "Error, something went wrong" });
514
+ }
515
+ });
516
+
517
+ routerAuth.post("/register", new Validator({ password_confirmation: { required: true, min: 6 }, name: { required: true }, email: { required: true, email: true }, password: { required: true, min: 6 } }).middleware(), async (req, res) => {
518
+ try {
519
+ const { name, email, password, password_confirmation } = req.body;
520
+ if (password !== password_confirmation) return res.status(400).json({ message: "Error, check your password confirmation", password_confirmation_error: true });
521
+ const exists = await AuthService.findUserByEmail(email);
522
+ if (exists) return res.status(400).json({ message: "Error, check your email entered", error_email_register: true });
523
+ const deletedAccount = await AuthService.findUserByEmail(email, 0);
524
+ if (deletedAccount) return res.status(400).json({ message: "Error, your account is deleted", deleted_account_error: true });
525
+ const user = await AuthService.createUser(name, email, password);
526
+ const secure = props.debug == false ? true : false;
527
+ const token = Auth.generateToken({ id: user.id_public, email: user.email });
528
+ res.cookie("token", token, { httpOnly: true, secure: secure, sameSite: "strict", maxAge: 24 * 60 * 60 * 1000 });
529
+ return res.json({ message: "Registered", user: { id: user.id_public, email: user.email } });
530
+ } catch (err) {
531
+ ;
532
+ return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
533
+ }
534
+ });
535
+
536
+ routerAuth.post("/forgot-password", new Validator({ email: { required: true, email: true } }).middleware(), async (req, res) => {
537
+ try {
538
+ const { email } = req.body;
539
+ const user = await AuthService.findUserByEmail(email, 1);
540
+
541
+ if (user) {
542
+ const token = crypto.randomBytes(32).toString('hex');
543
+ await AuthService.setPasswordToken(user.id, token);
544
+
545
+ if (props.debug) {
546
+ console.log("\\n[DEBUG] Email functionality for Forgot Password should be handled here.");
547
+ console.log("[DEBUG] Debug mode active. Password reset link: " + props.host + ":" + props.port + "/reset-password?token=" + token + "\\n");
548
+ }
549
+ }
550
+
551
+ return res.json({ message: "If the email is valid, a password reset link has been sent." });
552
+ } catch (err) {
553
+ return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
554
+ }
555
+ });
556
+
557
+ routerAuth.post("/reset-password/validate", async (req, res) => {
558
+ try {
559
+ const { token } = req.body;
560
+ if (!token) {
561
+ return res.status(400).json({ title: "Reset Password", queryToken: false })
562
+ }
563
+ const user = await AuthService.findUserByPasswordToken(token, 1);
564
+ if (!user) {
565
+ return res.status(400).json({ title: "Reset Password", queryToken: false, u: false })
566
+ }
567
+ return res.json({ title: "Reset Password", queryToken: true })
568
+ } catch (err) {
569
+ return res.status(500).json({ title: "Reset Password", queryToken: false })
570
+ }
571
+ });
572
+
573
+ routerAuth.post("/reset-password", new Validator({ password_confirmation: { required: true, min: 6 }, token: { required: true }, password: { required: true, min: 6 } }).middleware(), async (req, res) => {
574
+ try {
575
+ const { token, password, password_confirmation } = req.body;
576
+ const user = await AuthService.findUserByPasswordToken(token, 1);
577
+ if (password !== password_confirmation) return res.status(400).json({ message: "Error, check your password confirmation", password_confirmation: true });
578
+
579
+ if (!user) {
580
+ return res.status(400).json({ message: "Invalid or expired reset token.", token: true });
581
+ }
582
+
583
+ const updatePassword = await AuthService.updatePassword(user.id, password);
584
+
585
+ if (!updatePassword) {
586
+ return res.status(400).json({ message: "Error, something went wrong", updatePassword: true });
587
+ }
588
+ await AuthService.setPasswordToken(user.id, null);
589
+
590
+ return res.json({ message: "Password has been reset successfully." });
591
+ } catch (err) {
592
+ return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
593
+ }
594
+ });
595
+
596
+ routerAuth.get("/validate", Auth.protect(), (req, res) => {
597
+ return res.json({ user: req.user });
598
+ });
599
+
600
+ routerAuth.get("/logout", async (req, res) => {
601
+ try {
602
+ res.clearCookie("token");
603
+ return res.json({ message: "Logged out" });
604
+ } catch (err) {
605
+ return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
606
+ }
607
+ });
608
+
609
+ routerAuth.post("/logout", async (req, res) => {
610
+ try {
611
+ res.clearCookie("token");
612
+ return res.json({ message: "Logged out" });
613
+ } catch (err) {
614
+ return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
615
+ }
616
+ });
617
+
618
+ export default routerAuth;
619
+ `;
620
+ fs.writeFileSync(authFile, authContent, "utf-8");
621
+
622
+ const authenticatedContent = `import Router from "@seip/blue-bird/core/router.js";
623
+ import Template from "@seip/blue-bird/core/template.js";
624
+ import Auth from "@seip/blue-bird/core/auth.js";
625
+
626
+ const routerAuthenticated = new Router();
627
+
628
+ routerAuthenticated.get("/dashboard", Auth.protect({ redirect: "/login" }), (req, res) => {
629
+ return Template.renderReact(res, "App", { title: "Dashboard" });
630
+ });
631
+
632
+ export default routerAuthenticated;
633
+ `;
634
+ fs.writeFileSync(authenticatedFile, authenticatedContent, "utf-8");
635
+ console.log(chalk.green("✓ Backend auth and authenticated routes generated."));
636
+ }
637
+
638
+ createFrontendComponents() {
639
+ const pagesDir = path.join(this.frontendDir, "resources", "js", "pages", "auth");
640
+ if (!fs.existsSync(pagesDir)) fs.mkdirSync(pagesDir, { recursive: true });
641
+
642
+ const loginContent = `import { useState } from 'react';
643
+ import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
644
+ import { Link } from 'react-router-dom';
645
+ import Card from '../../blue-bird/components/Card.jsx';
646
+ import Input from '../../blue-bird/components/Input.jsx';
647
+ import Button from '../../blue-bird/components/Button.jsx';
648
+ import Typography from '../../blue-bird/components/Typography.jsx';
649
+
650
+ export default function Login() {
651
+ const { t, lang, setLang } = useLanguage();
652
+ const [email, setEmail] = useState('');
653
+ const [password, setPassword] = useState('');
654
+ const [error, setError] = useState(null);
655
+
656
+ const handleSubmit = async (e) => {
657
+ e.preventDefault();
658
+ const lang = localStorage.getItem("blue_bird_lang") ?? "en";
659
+ try {
660
+ const res = await fetch('/auth/login', {
661
+ method: 'POST',
662
+ headers: { 'Content-Type': 'application/json' },
663
+ body: JSON.stringify({ email, password , lang })
664
+ });
665
+ const data = await res.json();
666
+ if (!res.ok) {
667
+ if (data.deleted_account_error) throw new Error(t('deleted_account_error'));
668
+ throw new Error(t('error_login') || t('error_general') || data.message);
669
+ }
670
+ window.location.href = '/dashboard';
671
+ } catch (err) {
672
+ setError(err.message);
673
+ }
674
+ };
675
+
676
+ return (
677
+ <div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
678
+ <Card className="w-full max-w-md">
679
+ <div className="mb-6 text-center">
680
+ <Typography variant="h3">{t('login')}</Typography>
681
+ </div>
682
+ {error && <div className="bg-red-100 text-red-700 p-3 mb-4 rounded-md text-sm">{error}</div>}
683
+ <form onSubmit={handleSubmit} className="space-y-4">
684
+ <Input label={t('email')} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
685
+ <Input label={t('password')} type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
686
+ <Button type="submit" className="w-full mt-2">{t('submit')}</Button>
687
+ </form>
688
+ <div className="mt-6 flex flex-col space-y-2 text-center text-sm">
689
+ <Link to="/forgot-password" className="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:underline">{t('forgot_password')}</Link>
690
+ <Link to="/register" className="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:underline">{t('dont_have_account_register')}</Link>
691
+ </div>
692
+ <div className="mt-6 flex justify-center space-x-4 text-sm border-t dark:border-slate-800 pt-4">
693
+ <button onClick={() => setLang('en')} className={\`\${lang === 'en' ? 'font-semibold text-slate-900 dark:text-slate-100' : 'text-slate-500 dark:text-slate-400'}\`}>EN</button>
694
+ <button onClick={() => setLang('es')} className={\`\${lang === 'es' ? 'font-semibold text-slate-900 dark:text-slate-100' : 'text-slate-500 dark:text-slate-400'}\`}>ES</button>
695
+ </div>
696
+ </Card>
697
+ </div>
698
+ );
699
+ }
700
+ `;
701
+ fs.writeFileSync(path.join(pagesDir, "Login.jsx"), loginContent, "utf-8");
702
+
703
+ const registerContent = `import { useState } from 'react';
704
+ import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
705
+ import { Link } from 'react-router-dom';
706
+ import Card from '../../blue-bird/components/Card.jsx';
707
+ import Input from '../../blue-bird/components/Input.jsx';
708
+ import Button from '../../blue-bird/components/Button.jsx';
709
+ import Typography from '../../blue-bird/components/Typography.jsx';
710
+
711
+ export default function Register() {
712
+ const { t, lang, setLang } = useLanguage();
713
+ const [name, setName] = useState('');
714
+ const [email, setEmail] = useState('');
715
+ const [password, setPassword] = useState('');
716
+ const [password_confirmation, setPasswordConfirmation] = useState('');
717
+ const [error, setError] = useState(null);
718
+ const [message, setMessage] = useState(null);
719
+
720
+ const handleSubmit = async (e) => {
721
+ e.preventDefault();
722
+ setError(null);
723
+ setMessage(null);
724
+ const lang = localStorage.getItem("blue_bird_lang") ?? "en";
725
+ try {
726
+ const res = await fetch('/auth/register', {
727
+ method: 'POST',
728
+ headers: { 'Content-Type': 'application/json' },
729
+ body: JSON.stringify({ name, email, password, password_confirmation, lang })
730
+ });
731
+ const data = await res.json();
732
+ if (!res.ok) {
733
+ if (data.password_confirmation_error) throw new Error(t('password_confirmation_err'));
734
+ if (data.deleted_account_error) throw new Error(t('deleted_account_error'));
735
+ if (data.error_email_register) throw new Error(t('error_email_register'));
736
+ throw new Error(data.message || t('error_general'));
737
+ }
738
+ else{
739
+ setMessage(t('register_success') || 'Register success');
740
+ setEmail('');
741
+ setName('');
742
+ setPassword('');
743
+ setPasswordConfirmation('');
744
+ setTimeout(() => {
745
+ window.location.href = '/login';
746
+ }, 4000);
747
+ }
748
+ } catch (err) {
749
+ setError(err.message);
750
+ }
751
+ };
752
+
753
+ return (
754
+ <div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
755
+ <Card className="w-full max-w-md">
756
+ <div className="mb-6 text-center">
757
+ <Typography variant="h3">{t('register')}</Typography>
758
+ </div>
759
+ {message && <div className="bg-green-100 text-green-700 p-3 mb-4 rounded-md text-sm">{message}</div>}
760
+ {error && <div className="bg-red-100 text-red-700 p-3 mb-4 rounded-md text-sm">{error}</div>}
761
+ <form onSubmit={handleSubmit} className="space-y-4">
762
+ <Input label={t('name')} type="text" value={name} onChange={(e) => setName(e.target.value)} required />
763
+ <Input label={t('email')} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
764
+ <Input label={t('password')} type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
765
+ <Input label={t('password_confirmation') || 'Confirm Password'} type="password" value={password_confirmation} onChange={(e) => setPasswordConfirmation(e.target.value)} required />
766
+
767
+ <Button type="submit" className="w-full mt-2">{t('submit')}</Button>
768
+ </form>
769
+ <div className="mt-6 text-center text-sm">
770
+ <Link to="/login" className="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:underline">{t('back_to_login') || 'Back to login'}</Link>
771
+ </div>
772
+ <div className="mt-6 flex justify-center space-x-4 text-sm border-t dark:border-slate-800 pt-4">
773
+ <button onClick={() => setLang('en')} className={\`\${lang === 'en' ? 'font-semibold text-slate-900 dark:text-slate-100' : 'text-slate-500 dark:text-slate-400'}\`}>EN</button>
774
+ <button onClick={() => setLang('es')} className={\`\${lang === 'es' ? 'font-semibold text-slate-900 dark:text-slate-100' : 'text-slate-500 dark:text-slate-400'}\`}>ES</button>
775
+ </div>
776
+ </Card>
777
+ </div>
778
+ );
779
+ }
780
+ `;
781
+ fs.writeFileSync(path.join(pagesDir, "Register.jsx"), registerContent, "utf-8");
782
+
783
+ const forgotPasswordContent = `import { useState } from 'react';
784
+ import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
785
+ import { Link } from 'react-router-dom';
786
+ import Card from '../../blue-bird/components/Card.jsx';
787
+ import Input from '../../blue-bird/components/Input.jsx';
788
+ import Button from '../../blue-bird/components/Button.jsx';
789
+ import Typography from '../../blue-bird/components/Typography.jsx';
790
+
791
+ export default function ForgotPassword() {
792
+ const { t, lang, setLang } = useLanguage();
793
+ const [email, setEmail] = useState('');
794
+ const [message, setMessage] = useState(null);
795
+ const [error, setError] = useState(null);
796
+
797
+ const handleSubmit = async (e) => {
798
+ e.preventDefault();
799
+ setMessage(null);
800
+ setError(null);
801
+ try {
802
+ const res = await fetch('/auth/forgot-password', {
803
+ method: 'POST',
804
+ headers: { 'Content-Type': 'application/json' },
805
+ body: JSON.stringify({ email })
806
+ });
807
+ const data = await res.json();
808
+ if (!res.ok) throw new Error(data.message || t('error_general'));
809
+ setMessage(t('If the email is valid, a password reset link has been sent') || data.message);
810
+ } catch (err) {
811
+ setError(err.message);
812
+ }
813
+ };
814
+
815
+ return (
816
+ <div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
817
+ <Card className="w-full max-w-md">
818
+ <div className="mb-4 text-center">
819
+ <Typography variant="h3">{t('forgot_password') || 'Forgot Password'}</Typography>
820
+ </div>
821
+ <div className="mb-6 text-center">
822
+ <Typography variant="muted">{t('forgot_password_desc') || 'Forgot your password? No problem. Just let us know your email address and we will email you a password reset link that will allow you to choose a new one.'}</Typography>
823
+ </div>
824
+ {error && <div className="bg-red-100 text-red-700 p-3 mb-4 rounded-md text-sm">{error}</div>}
825
+ {message && <div className="bg-green-100 text-green-700 p-3 mb-4 rounded-md text-sm">{message}</div>}
826
+ <form onSubmit={handleSubmit} className="space-y-4">
827
+ <Input label={t('email')} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
828
+ <Button type="submit" className="w-full mt-2">{t('submit')}</Button>
829
+ </form>
830
+ <div className="mt-6 text-center text-sm">
831
+ <Link to="/login" className="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:underline">{t('back_to_login') || 'Back to login'}</Link>
832
+ </div>
833
+ </Card>
834
+ </div>
835
+ );
836
+ }
837
+ `;
838
+ fs.writeFileSync(path.join(pagesDir, "ForgotPassword.jsx"), forgotPasswordContent, "utf-8");
839
+
840
+ const resetPasswordContent = `import { useState, useEffect } from 'react';
841
+ import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
842
+ import { useLocation, Link } from 'react-router-dom';
843
+ import Card from '../../blue-bird/components/Card.jsx';
844
+ import Input from '../../blue-bird/components/Input.jsx';
845
+ import Button from '../../blue-bird/components/Button.jsx';
846
+ import Typography from '../../blue-bird/components/Typography.jsx';
847
+
848
+ export default function ResetPassword() {
849
+ const { t, lang, setLang } = useLanguage();
850
+ const [password, setPassword] = useState('');
851
+ const [password_confirmation, setPasswordConfirmation] = useState('');
852
+ const [token, setToken] = useState('');
853
+ const [message, setMessage] = useState(null);
854
+ const [error, setError] = useState(null);
855
+
856
+ const search = useLocation().search;
857
+ useEffect(() => {
858
+ const urlParams = new URLSearchParams(search);
859
+ const t = urlParams.get('token');
860
+ if (t) setToken(t);
861
+ }, [search]);
862
+
863
+ useEffect(() => {
864
+ if (token) {
865
+ const fetchUser = async () => {
866
+ const res = await fetch('/auth/reset-password/validate', {
867
+ method: 'POST',
868
+ headers: { 'Content-Type': 'application/json' },
869
+ body: JSON.stringify({ token })
870
+ });
871
+ if (!res.ok) return window.location.href = '/login';
872
+ };
873
+ fetchUser();
874
+ }
875
+ }, [token]);
876
+
877
+ const handleSubmit = async (e) => {
878
+ e.preventDefault();
879
+ setMessage(null);
880
+ setError(null);
881
+ try {
882
+ const res = await fetch('/auth/reset-password', {
883
+ method: 'POST',
884
+ headers: { 'Content-Type': 'application/json' },
885
+ body: JSON.stringify({ token, password, password_confirmation })
886
+ });
887
+ const data = await res.json();
888
+ if (!res.ok) {
889
+ if (data.password_confirmation) throw new Error(t('password_confirmation_err'));
890
+ if (data.token) throw new Error(t('error_token_reset') || data.message);
891
+ throw new Error(data.message || t('error_general'));
892
+ }
893
+ setMessage(data.message);
894
+ setTimeout(() => { window.location.href = '/login'; }, 2000);
895
+ } catch (err) {
896
+ setError(err.message);
897
+ }
898
+ };
899
+
900
+ return (
901
+ <div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
902
+ <Card className="w-full max-w-md">
903
+ <div className="mb-6 text-center">
904
+ <Typography variant="h3">Reset Password</Typography>
905
+ </div>
906
+ {error && <div className="bg-red-100 text-red-700 p-3 mb-4 rounded-md text-sm">{error}</div>}
907
+ {message && <div className="bg-green-100 text-green-700 p-3 mb-4 rounded-md text-sm">{message}</div>}
908
+ <form onSubmit={handleSubmit} className="space-y-4">
909
+ <input type="hidden" value={token} required />
910
+ <Input label={'New ' + (t('password') || 'Password')} type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
911
+ <Input label={t('password_confirmation') || 'Confirm Password'} type="password" value={password_confirmation} onChange={(e) => setPasswordConfirmation(e.target.value)} required />
912
+ <Button type="submit" className="w-full mt-2">{t('submit')}</Button>
913
+ </form>
914
+ <div className="mt-6 text-center text-sm">
915
+ <Link to="/login" className="text-slate-600 dark:text-slate-400 hover:text-slate-900 dark:hover:text-slate-200 hover:underline">{t('back_to_login') || 'Back to login'}</Link>
916
+ </div>
917
+ </Card>
918
+ </div>
919
+ );
920
+ }
921
+
922
+ `;
923
+ fs.writeFileSync(path.join(pagesDir, "ResetPassword.jsx"), resetPasswordContent, "utf-8");
924
+
925
+ const dashboardContent = ` import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
926
+ import { useEffect, useState } from 'react';
927
+ import Button from '../../blue-bird/components/Button.jsx';
928
+ import Typography from '../../blue-bird/components/Typography.jsx';
929
+ import Card from '../../blue-bird/components/Card.jsx';
930
+
931
+ export default function Dashboard() {
932
+ const { t, lang, setLang } = useLanguage();
933
+ const [user, setUser] = useState(null);
934
+
935
+ useEffect(() => {
936
+ const fetchUser = async () => {
937
+ const res = await fetch('/auth/validate');
938
+ if (!res.ok) return window.location.href = '/login';
939
+ const data = await res.json();
940
+ setUser(data.user);
941
+ };
942
+ fetchUser();
943
+ }, []);
944
+
945
+ const logout = async () => {
946
+ const res = await fetch('/auth/logout');
947
+ if (!res.ok) throw new Error(t('error_general'));
948
+ return window.location.href = '/login';
949
+ };
950
+
951
+ if (!user) return <div className="min-h-screen flex items-center justify-center bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100"><Typography variant="p">Loading...</Typography></div>;
952
+
953
+ return (
954
+ <div className="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100 flex flex-col">
955
+ <header className="bg-white dark:bg-slate-900 border-b dark:border-slate-800 px-6 py-4 flex justify-between items-center sticky top-0 z-10 shadow-sm">
956
+ <Typography variant="h4">{t('dashboard')}</Typography>
957
+ <div className="flex items-center space-x-4">
958
+ <button onClick={() => setLang('en')} className={\`text-sm transition-colors hover:text-slate-900 dark:hover:text-slate-100 \${lang === 'en' ? 'font-semibold text-slate-900 dark:text-slate-100' : 'text-slate-500 dark:text-slate-400'}\`}>EN</button>
959
+ <button onClick={() => setLang('es')} className={\`text-sm transition-colors hover:text-slate-900 dark:hover:text-slate-100 \${lang === 'es' ? 'font-semibold text-slate-900 dark:text-slate-100' : 'text-slate-500 dark:text-slate-400'}\`}>ES</button>
960
+ <div className="w-px h-4 bg-slate-200 dark:bg-slate-700 mx-2"></div>
961
+ <Button variant="ghost" onClick={logout} className="text-red-600 dark:text-red-400 hover:text-red-700 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30">{t('logout')}</Button>
962
+ </div>
963
+ </header>
964
+ <main className="flex-1 p-8 max-w-7xl mx-auto w-full">
965
+ <Card>
966
+ <Typography variant="h3" className="mb-2">Welcome, {user.email}!</Typography>
967
+ <Typography variant="muted">You are successfully logged into your dashboard.</Typography>
968
+ </Card>
969
+ </main>
970
+ </div>
971
+ );
972
+ }
973
+
974
+ `;
975
+ const authenticatedPagesDir = path.join(this.frontendDir, "resources", "js", "pages", "authenticated");
976
+ if (!fs.existsSync(authenticatedPagesDir)) fs.mkdirSync(authenticatedPagesDir, { recursive: true });
977
+ fs.writeFileSync(path.join(authenticatedPagesDir, "Dashboard.jsx"), dashboardContent, "utf-8");
978
+
979
+ console.log(chalk.green("✓ Frontend React UI Components generated."));
980
+ }
981
+
982
+ modifyEntryFiles() {
983
+ const backendIndexFile = path.join(this.backendDir, "index.js");
984
+ if (fs.existsSync(backendIndexFile)) {
985
+ let backendIndex = fs.readFileSync(backendIndexFile, "utf-8");
986
+ if (!backendIndex.includes("routerAuth")) {
987
+ backendIndex = backendIndex.replace(
988
+ /import\s+routerFrontendExample\s+from\s+["']\.\/routes\/frontend\.js["'];?/,
989
+ 'import routerFrontendExample from "./routes/frontend.js";\nimport routerAuth from "./routes/auth.js";\nimport routerAuthenticated from "./routes/authenticated.js";'
990
+ );
991
+ backendIndex = backendIndex.replace(
992
+ /routes:\s*\[([^\]]+)\]/,
993
+ (match, p1) => {
994
+ if (p1.includes('routerFrontendExample') && !p1.includes('routerAuth')) {
995
+ return `routes: [${p1.replace('routerFrontendExample', 'routerAuth, routerAuthenticated, routerFrontendExample')}]`;
996
+ }
997
+ return match;
998
+ }
999
+ );
1000
+ fs.writeFileSync(backendIndexFile, backendIndex, "utf-8");
1001
+ console.log(chalk.green("✓ backend/index.js updated to include new routes."));
1002
+ }
1003
+ }
1004
+
1005
+ const appJsxFile = path.join(this.frontendDir, "resources", "js", "App.jsx");
1006
+ if (fs.existsSync(appJsxFile)) {
1007
+ let appJsx = fs.readFileSync(appJsxFile, "utf-8");
1008
+ if (!appJsx.includes("LanguageProvider")) {
1009
+ appJsx = appJsx.replace(
1010
+ /import\s+Home\s+from\s+["']\.\/pages\/Home["'];?/,
1011
+ "import { LanguageProvider } from './blue-bird/contexts/LanguageContext.jsx';\nimport Login from './pages/auth/Login.jsx';\nimport Register from './pages/auth/Register.jsx';\nimport ForgotPassword from './pages/auth/ForgotPassword.jsx';\nimport ResetPassword from './pages/auth/ResetPassword.jsx';\nimport Dashboard from './pages/authenticated/Dashboard.jsx';\nimport Home from './pages/Home';"
1012
+ );
1013
+ appJsx = appJsx.replace(
1014
+ /<Router>/,
1015
+ "<LanguageProvider>\n <Router>"
1016
+ );
1017
+ appJsx = appJsx.replace(
1018
+ /<\/Router>/,
1019
+ "</Router>\n </LanguageProvider>"
1020
+ );
1021
+ appJsx = appJsx.replace(
1022
+ /<Route\s+path=["']\/about["']\s+element=\{<About\s*\/?>\}\s*\/?>/,
1023
+ '<Route path="/about" element={<About />} />\n <Route path="/login" element={<Login />} />\n <Route path="/register" element={<Register />} />\n <Route path="/forgot-password" element={<ForgotPassword />} />\n <Route path="/reset-password" element={<ResetPassword />} />\n <Route path="/dashboard" element={<Dashboard />} />'
1024
+ );
1025
+ fs.writeFileSync(appJsxFile, appJsx, "utf-8");
1026
+ console.log(chalk.green("✓ frontend/resources/js/App.jsx updated with routes and Provider."));
1027
+ }
1028
+ }
1029
+ }
1030
+ }
1031
+
1032
+ const initializer = new ScaffoldingAuth();
1033
+ export default initializer;
1034
+
1035
+ if (process.argv[1] === fileURLToPath(import.meta.url)) {
1036
+ initializer.run();
1037
+ }
package/core/swagger.js CHANGED
@@ -1,25 +1,40 @@
1
- import swaggerUi from "swagger-ui-express";
2
- import swaggerJSDoc from "swagger-jsdoc";
3
-
4
- class Swagger {
5
-
6
- static init(app, options) {
7
-
8
- const optionsJsDoc = {
9
- definition: {
10
- openapi: "3.0.0",
11
- info: options.info,
12
- servers: [
13
- { url: options.url }
14
- ]
15
- },
16
- apis: ["./backend/routes/*.js"]
17
- };
18
-
19
- const specs = swaggerJSDoc(optionsJsDoc);
20
-
21
- app.use("/docs", swaggerUi.serve, swaggerUi.setup(specs));
22
- }
23
- }
24
-
25
- export default Swagger;
1
+ import { execSync } from "node:child_process";
2
+
3
+ class SwaggerCli {
4
+ install() {
5
+ const dependencies = this.checkDependencies();
6
+ if (dependencies.missingDependencies.length > 0) {
7
+ console.log("Installing dependencies...");
8
+ console.log(`Installing swagger-jsdoc...`);
9
+ execSync(`npm install swagger-jsdoc@6.2.8`, { stdio: "inherit" });
10
+ console.log(`Installing swagger-ui-express...`);
11
+ execSync(`npm install swagger-ui-express@5.0.1`, { stdio: "inherit" });
12
+ }
13
+ }
14
+ checkDependencies() {
15
+ const dependencies = [
16
+ "swagger-jsdoc",
17
+ "swagger-ui-express"
18
+ ];
19
+ const missingDependencies = [];
20
+ dependencies.forEach(dependency => {
21
+ if (!this.checkDependency(dependency)) {
22
+ missingDependencies.push(dependency);
23
+ }
24
+ });
25
+ return {
26
+ missingDependencies
27
+ };
28
+ }
29
+ checkDependency(dependency) {
30
+ try {
31
+ require.resolve(dependency);
32
+ return true;
33
+ } catch (error) {
34
+ return false;
35
+ }
36
+ }
37
+ }
38
+
39
+ const swaggerExecutor = new SwaggerCli();
40
+ swaggerExecutor.install();
File without changes
File without changes
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seip/blue-bird",
3
- "version": "0.4.4",
3
+ "version": "0.4.5",
4
4
  "description": "Express + React opinionated framework with SPA or API architecture and built-in JWT auth",
5
5
  "type": "module",
6
6
  "bin": {