@seip/blue-bird 0.3.1 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env_example +23 -13
- package/LICENSE +21 -21
- package/README.md +79 -79
- package/backend/index.js +12 -12
- package/backend/routes/api.js +34 -34
- package/backend/routes/frontend.js +1 -8
- package/core/app.js +359 -359
- package/core/auth.js +69 -69
- package/core/cache.js +35 -35
- package/core/cli/component.js +42 -42
- package/core/cli/init.js +120 -118
- package/core/cli/react.js +383 -409
- package/core/cli/route.js +42 -42
- package/core/cli/scaffolding-auth.js +967 -0
- package/core/config.js +41 -41
- package/core/debug.js +248 -248
- package/core/logger.js +80 -80
- package/core/middleware.js +27 -27
- package/core/router.js +134 -134
- package/core/swagger.js +24 -24
- package/core/template.js +288 -288
- package/core/upload.js +76 -76
- package/core/validate.js +291 -290
- package/frontend/index.html +28 -22
- package/frontend/resources/js/App.jsx +28 -42
- package/frontend/resources/js/Main.jsx +17 -17
- package/frontend/resources/js/blue-bird/components/Button.jsx +67 -0
- package/frontend/resources/js/blue-bird/components/Card.jsx +17 -0
- package/frontend/resources/js/blue-bird/components/DataTable.jsx +126 -0
- package/frontend/resources/js/blue-bird/components/Input.jsx +21 -0
- package/frontend/resources/js/blue-bird/components/Label.jsx +12 -0
- package/frontend/resources/js/blue-bird/components/Modal.jsx +27 -0
- package/frontend/resources/js/blue-bird/components/Translate.jsx +12 -0
- package/frontend/resources/js/blue-bird/components/Typography.jsx +25 -0
- package/frontend/resources/js/blue-bird/contexts/LanguageContext.jsx +29 -0
- package/frontend/resources/js/blue-bird/contexts/SnackbarContext.jsx +38 -0
- package/frontend/resources/js/blue-bird/contexts/ThemeContext.jsx +49 -0
- package/frontend/resources/js/blue-bird/locales/en.json +30 -0
- package/frontend/resources/js/blue-bird/locales/es.json +30 -0
- package/frontend/resources/js/pages/About.jsx +33 -15
- package/frontend/resources/js/pages/Home.jsx +93 -68
- package/package.json +56 -55
- package/vite.config.js +21 -21
|
@@ -0,0 +1,967 @@
|
|
|
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 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\nasync function setup() {\n`;
|
|
229
|
+
|
|
230
|
+
let usersTable = "";
|
|
231
|
+
let historyTable = "";
|
|
232
|
+
|
|
233
|
+
if (dialect === "sqlite") {
|
|
234
|
+
usersTable = `
|
|
235
|
+
await db.query(\`
|
|
236
|
+
CREATE TABLE IF NOT EXISTS User (
|
|
237
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
238
|
+
id_public TEXT UNIQUE NOT NULL,
|
|
239
|
+
name TEXT NOT NULL,
|
|
240
|
+
email TEXT UNIQUE NOT NULL,
|
|
241
|
+
is_active INTEGER DEFAULT 1,
|
|
242
|
+
password TEXT NOT NULL,
|
|
243
|
+
password_token TEXT,
|
|
244
|
+
remember_token TEXT,
|
|
245
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
246
|
+
)
|
|
247
|
+
\`);`;
|
|
248
|
+
|
|
249
|
+
historyTable = `
|
|
250
|
+
await db.query(\`
|
|
251
|
+
CREATE TABLE IF NOT EXISTS LoginHistory (
|
|
252
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
253
|
+
email TEXT NOT NULL,
|
|
254
|
+
ip_address TEXT NOT NULL,
|
|
255
|
+
success INTEGER NOT NULL,
|
|
256
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
257
|
+
)
|
|
258
|
+
\`);`;
|
|
259
|
+
} else if (dialect === "mysql") {
|
|
260
|
+
usersTable = `
|
|
261
|
+
await db.query(\`
|
|
262
|
+
CREATE TABLE IF NOT EXISTS User (
|
|
263
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
264
|
+
id_public VARCHAR(36) UNIQUE NOT NULL,
|
|
265
|
+
name VARCHAR(255) NOT NULL,
|
|
266
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
267
|
+
is_active BOOLEAN DEFAULT TRUE,
|
|
268
|
+
password VARCHAR(255) NOT NULL,
|
|
269
|
+
password_token VARCHAR(255),
|
|
270
|
+
remember_token VARCHAR(255),
|
|
271
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
272
|
+
)
|
|
273
|
+
\`);`;
|
|
274
|
+
|
|
275
|
+
historyTable = `
|
|
276
|
+
await db.query(\`
|
|
277
|
+
CREATE TABLE IF NOT EXISTS LoginHistory (
|
|
278
|
+
id INT AUTO_INCREMENT PRIMARY KEY,
|
|
279
|
+
email VARCHAR(255) NOT NULL,
|
|
280
|
+
ip_address VARCHAR(45) NOT NULL,
|
|
281
|
+
success BOOLEAN NOT NULL,
|
|
282
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
283
|
+
)
|
|
284
|
+
\`);`;
|
|
285
|
+
} else if (dialect === "postgres") {
|
|
286
|
+
usersTable = `
|
|
287
|
+
await db.query(\`
|
|
288
|
+
CREATE TABLE IF NOT EXISTS "User" (
|
|
289
|
+
id SERIAL PRIMARY KEY,
|
|
290
|
+
id_public UUID UNIQUE NOT NULL,
|
|
291
|
+
name VARCHAR(255) NOT NULL,
|
|
292
|
+
email VARCHAR(255) UNIQUE NOT NULL,
|
|
293
|
+
is_active BOOLEAN DEFAULT TRUE,
|
|
294
|
+
password VARCHAR(255) NOT NULL,
|
|
295
|
+
password_token VARCHAR(255),
|
|
296
|
+
remember_token VARCHAR(255),
|
|
297
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
298
|
+
)
|
|
299
|
+
\`);`;
|
|
300
|
+
|
|
301
|
+
historyTable = `
|
|
302
|
+
await db.query(\`
|
|
303
|
+
CREATE TABLE IF NOT EXISTS "LoginHistory" (
|
|
304
|
+
id SERIAL PRIMARY KEY,
|
|
305
|
+
email VARCHAR(255) NOT NULL,
|
|
306
|
+
ip_address VARCHAR(45) NOT NULL,
|
|
307
|
+
success BOOLEAN NOT NULL,
|
|
308
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
|
309
|
+
)
|
|
310
|
+
\`);`;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
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`;
|
|
314
|
+
fs.writeFileSync(path.join(dbDir, "setup_tables.js"), content, "utf-8");
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
generateAuthService(servicesDir, dialect) {
|
|
318
|
+
let content = `import db from '../connection.js';
|
|
319
|
+
import crypto from 'node:crypto';
|
|
320
|
+
import { hash } from 'bcrypt';
|
|
321
|
+
|
|
322
|
+
export default class AuthService {
|
|
323
|
+
static async checkRateLimit(ip, email) {
|
|
324
|
+
const tenMinsAgo = new Date(Date.now() - 10 * 60 * 1000).toISOString();
|
|
325
|
+
`;
|
|
326
|
+
|
|
327
|
+
if (dialect === "postgres") {
|
|
328
|
+
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`;
|
|
329
|
+
} else {
|
|
330
|
+
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`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
content += ` const attempts = result ? parseInt(result.count || result.COUNT) : 0;
|
|
334
|
+
|
|
335
|
+
if (attempts >= 15) throw new Error("Blocked for 10 minutes.");
|
|
336
|
+
if (attempts >= 10) throw new Error("Blocked for 5 minutes.");
|
|
337
|
+
if (attempts >= 8) throw new Error("Blocked for 3 minutes.");
|
|
338
|
+
if (attempts >= 5) throw new Error("Blocked for 1 minute.");
|
|
339
|
+
return true;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
static async findUserByEmail(email, isActive = 1) {
|
|
343
|
+
`;
|
|
344
|
+
if (dialect === "postgres") {
|
|
345
|
+
content += ` return await db.queryOne('SELECT * FROM "User" WHERE is_active = $1 AND email = $2', [isActive, email]);\n`;
|
|
346
|
+
} else {
|
|
347
|
+
content += ` return await db.queryOne('SELECT * FROM User WHERE is_active = ? AND email = ?', [isActive, email]);\n`;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
content += ` }
|
|
351
|
+
|
|
352
|
+
static async createLoginHistory(email, ip, success) {
|
|
353
|
+
`;
|
|
354
|
+
if (dialect === "postgres") {
|
|
355
|
+
content += ` await db.query('INSERT INTO "LoginHistory" (email, ip_address, success) VALUES ($1, $2, $3)', [email, ip, success]);\n`;
|
|
356
|
+
} else {
|
|
357
|
+
content += ` const successVal = success ? 1 : 0;\n await db.query('INSERT INTO LoginHistory (email, ip_address, success) VALUES (?, ?, ?)', [email, ip, successVal]);\n`;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
content += ` }
|
|
361
|
+
|
|
362
|
+
static async createUser(name, email, password) {
|
|
363
|
+
const id_public = crypto.randomUUID();
|
|
364
|
+
const passwordHash= await hash(password, 10);
|
|
365
|
+
`;
|
|
366
|
+
|
|
367
|
+
if (dialect === "postgres") {
|
|
368
|
+
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`;
|
|
369
|
+
} else {
|
|
370
|
+
content += ` await db.query('INSERT INTO User (id_public, name, email, password, is_active) VALUES (?, ?, ?, ?, ?)', [id_public, name, email, passwordHash, true]);\n`;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
content += ` return { id_public, name, email };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
static async setPasswordToken(userId, token) {
|
|
377
|
+
`;
|
|
378
|
+
if (dialect === "postgres") {
|
|
379
|
+
content += ` await db.query('UPDATE "User" SET password_token = $1 WHERE id = $2', [token, userId]);\n`;
|
|
380
|
+
} else {
|
|
381
|
+
content += ` await db.query('UPDATE User SET password_token = ? WHERE id = ?', [token, userId]);\n`;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
content += ` }
|
|
385
|
+
|
|
386
|
+
static async findUserByPasswordToken(token, isActive = 1) {
|
|
387
|
+
`;
|
|
388
|
+
if (dialect === "postgres") {
|
|
389
|
+
content += ` return await db.queryOne('SELECT * FROM "User" WHERE password_token = $1 AND is_active = $2', [token, isActive]);\n`;
|
|
390
|
+
} else {
|
|
391
|
+
content += ` return await db.queryOne('SELECT * FROM User WHERE password_token = ? AND is_active = ?', [token, isActive]);\n`;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
content += ` }
|
|
395
|
+
|
|
396
|
+
static async updatePassword(userId, newPassword) {
|
|
397
|
+
const newPasswordHash = await hash(newPassword, 10);
|
|
398
|
+
`;
|
|
399
|
+
if (dialect === "postgres") {
|
|
400
|
+
content += ` return await db.query('UPDATE "User" SET password = $1, password_token = NULL WHERE id = $2', [newPasswordHash, userId]);\n`;
|
|
401
|
+
} else {
|
|
402
|
+
content += ` return await db.query('UPDATE User SET password = ?, password_token = NULL WHERE id = ?', [newPasswordHash, userId]);\n`;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
content += ` }
|
|
406
|
+
}
|
|
407
|
+
`;
|
|
408
|
+
fs.writeFileSync(path.join(servicesDir, "auth.service.js"), content, "utf-8");
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
createBackendRoutes() {
|
|
412
|
+
const routesDir = path.join(this.backendDir, "routes");
|
|
413
|
+
if (!fs.existsSync(routesDir)) fs.mkdirSync(routesDir, { recursive: true });
|
|
414
|
+
|
|
415
|
+
const authFile = path.join(routesDir, "auth.js");
|
|
416
|
+
const authenticatedFile = path.join(routesDir, "authenticated.js");
|
|
417
|
+
|
|
418
|
+
const authContent = `import Router from "@seip/blue-bird/core/router.js";
|
|
419
|
+
import Validator from "@seip/blue-bird/core/validate.js";
|
|
420
|
+
import Auth from "@seip/blue-bird/core/auth.js";
|
|
421
|
+
import Config from "@seip/blue-bird/core/config.js";
|
|
422
|
+
import crypto from "node:crypto";
|
|
423
|
+
import AuthService from "../databases/services/auth.service.js";
|
|
424
|
+
import { compare } from "bcrypt";
|
|
425
|
+
import Template from "@seip/blue-bird/core/template.js";
|
|
426
|
+
|
|
427
|
+
const routerAuth = new Router("/auth");
|
|
428
|
+
const props = Config.props();
|
|
429
|
+
|
|
430
|
+
routerAuth.post("/login", new Validator({ email: { required: true, email: true }, password: { required: true } }).middleware(), async (req, res) => {
|
|
431
|
+
try {
|
|
432
|
+
const { email, password } = req.body;
|
|
433
|
+
const ip = req.ip;
|
|
434
|
+
|
|
435
|
+
await AuthService.checkRateLimit(ip, email);
|
|
436
|
+
|
|
437
|
+
const user = await AuthService.findUserByEmail(email, 1);
|
|
438
|
+
|
|
439
|
+
if (!user) {
|
|
440
|
+
await AuthService.createLoginHistory(email, ip, false);
|
|
441
|
+
const deletedAccount = await AuthService.findUserByEmail(email, 0);
|
|
442
|
+
if (deletedAccount) return res.status(401).json({ message: "Your account is deleted", deleted_account_error: true });
|
|
443
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
444
|
+
}
|
|
445
|
+
const passwordCompare = await compare(password, user.password);
|
|
446
|
+
|
|
447
|
+
if (!passwordCompare) {
|
|
448
|
+
await AuthService.createLoginHistory(email, ip, false);
|
|
449
|
+
return res.status(401).json({ message: "Invalid credentials" });
|
|
450
|
+
}
|
|
451
|
+
await AuthService.createLoginHistory(email, ip, true);
|
|
452
|
+
await AuthService.setPasswordToken(user.id, null);
|
|
453
|
+
const token = Auth.generateToken({ id: user.id_public, email: user.email });
|
|
454
|
+
const secure = props.debug == false ? true : false;
|
|
455
|
+
res.cookie("token", token, { httpOnly: true, secure: secure, sameSite: "strict", maxAge: 24 * 60 * 60 * 1000 });
|
|
456
|
+
return res.json({ user: { id: user.id_public, name: user.name, email: user.email } });
|
|
457
|
+
} catch (error) {
|
|
458
|
+
console.log(error)
|
|
459
|
+
return res.status(429).json({ message: props.debug ? error.message : "Error, something went wrong" });
|
|
460
|
+
}
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
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) => {
|
|
464
|
+
try {
|
|
465
|
+
const { name, email, password, password_confirmation } = req.body;
|
|
466
|
+
if (password !== password_confirmation) return res.status(400).json({ message: "Error, check your password confirmation", password_confirmation_error: true });
|
|
467
|
+
const exists = await AuthService.findUserByEmail(email);
|
|
468
|
+
if (exists) return res.status(400).json({ message: "Error, check your email entered", error_email_register: true });
|
|
469
|
+
const deletedAccount = await AuthService.findUserByEmail(email, 0);
|
|
470
|
+
if (deletedAccount) return res.status(400).json({ message: "Error, your account is deleted", deleted_account_error: true });
|
|
471
|
+
const user = await AuthService.createUser(name, email, password);
|
|
472
|
+
const secure = process.env.NODE_ENV === "production" ? true : false;
|
|
473
|
+
const token = Auth.generateToken({ id: user.id_public, email: user.email });
|
|
474
|
+
res.cookie("token", token, { httpOnly: true, secure: secure, sameSite: "strict", maxAge: 24 * 60 * 60 * 1000 });
|
|
475
|
+
return res.json({ message: "Registered", user: { id: user.id_public, email: user.email } });
|
|
476
|
+
} catch (err) {
|
|
477
|
+
;
|
|
478
|
+
return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
|
|
479
|
+
}
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
routerAuth.post("/forgot-password", new Validator({ email: { required: true, email: true } }).middleware(), async (req, res) => {
|
|
483
|
+
try {
|
|
484
|
+
const { email } = req.body;
|
|
485
|
+
const user = await AuthService.findUserByEmail(email, 1);
|
|
486
|
+
|
|
487
|
+
if (user) {
|
|
488
|
+
const token = crypto.randomBytes(32).toString('hex');
|
|
489
|
+
await AuthService.setPasswordToken(user.id, token);
|
|
490
|
+
|
|
491
|
+
if (props.debug) {
|
|
492
|
+
console.log("\\n[DEBUG] Email functionality for Forgot Password should be handled here.");
|
|
493
|
+
console.log("[DEBUG] Debug mode active. Password reset link: " + props.host + ":" + props.port + "/reset-password?token=" + token + "\\n");
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return res.json({ message: "If the email is valid, a password reset link has been sent." });
|
|
498
|
+
} catch (err) {
|
|
499
|
+
return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
routerAuth.post("/reset-password/validate", async (req, res) => {
|
|
504
|
+
try {
|
|
505
|
+
const { token } = req.body;
|
|
506
|
+
if (!token) {
|
|
507
|
+
return res.status(400).json({ title: "Reset Password", queryToken: false })
|
|
508
|
+
}
|
|
509
|
+
const user = await AuthService.findUserByPasswordToken(token, 1);
|
|
510
|
+
if (!user) {
|
|
511
|
+
return res.status(400).json({ title: "Reset Password", queryToken: false, u: false })
|
|
512
|
+
}
|
|
513
|
+
return res.json({ title: "Reset Password", queryToken: true })
|
|
514
|
+
} catch (err) {
|
|
515
|
+
return res.status(500).json({ title: "Reset Password", queryToken: false })
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
|
|
519
|
+
routerAuth.post("/reset-password", new Validator({ password_confirmation: { required: true, min: 6 }, token: { required: true }, password: { required: true, min: 6 } }).middleware(), async (req, res) => {
|
|
520
|
+
try {
|
|
521
|
+
const { token, password, password_confirmation } = req.body;
|
|
522
|
+
const user = await AuthService.findUserByPasswordToken(token, 1);
|
|
523
|
+
if (password !== password_confirmation) return res.status(400).json({ message: "Error, check your password confirmation", password_confirmation: true });
|
|
524
|
+
|
|
525
|
+
if (!user) {
|
|
526
|
+
return res.status(400).json({ message: "Invalid or expired reset token.", token: true });
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
const updatePassword = await AuthService.updatePassword(user.id, password);
|
|
530
|
+
|
|
531
|
+
if (!updatePassword) {
|
|
532
|
+
return res.status(400).json({ message: "Error, something went wrong", updatePassword: true });
|
|
533
|
+
}
|
|
534
|
+
await AuthService.setPasswordToken(user.id, null);
|
|
535
|
+
|
|
536
|
+
return res.json({ message: "Password has been reset successfully." });
|
|
537
|
+
} catch (err) {
|
|
538
|
+
return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
|
|
539
|
+
}
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
routerAuth.get("/validate", Auth.protect(), (req, res) => {
|
|
543
|
+
return res.json({ user: req.user });
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
routerAuth.get("/logout", async (req, res) => {
|
|
547
|
+
try {
|
|
548
|
+
res.clearCookie("token");
|
|
549
|
+
return res.json({ message: "Logged out" });
|
|
550
|
+
} catch (err) {
|
|
551
|
+
return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
|
|
555
|
+
routerAuth.post("/logout", async (req, res) => {
|
|
556
|
+
try {
|
|
557
|
+
res.clearCookie("token");
|
|
558
|
+
return res.json({ message: "Logged out" });
|
|
559
|
+
} catch (err) {
|
|
560
|
+
return res.status(500).json({ message: props.debug ? err.message : "Error, something went wrong" });
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
export default routerAuth;
|
|
565
|
+
`;
|
|
566
|
+
fs.writeFileSync(authFile, authContent, "utf-8");
|
|
567
|
+
|
|
568
|
+
const authenticatedContent = `import Router from "@seip/blue-bird/core/router.js";
|
|
569
|
+
import Template from "@seip/blue-bird/core/template.js";
|
|
570
|
+
import Auth from "@seip/blue-bird/core/auth.js";
|
|
571
|
+
|
|
572
|
+
const routerAuthenticated = new Router();
|
|
573
|
+
|
|
574
|
+
routerAuthenticated.get("/dashboard", Auth.protect({ redirect: "/login" }), (req, res) => {
|
|
575
|
+
return Template.renderReact(res, "App", { title: "Dashboard" });
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
export default routerAuthenticated;
|
|
579
|
+
`;
|
|
580
|
+
fs.writeFileSync(authenticatedFile, authenticatedContent, "utf-8");
|
|
581
|
+
console.log(chalk.green("✓ Backend auth and authenticated routes generated."));
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
createFrontendComponents() {
|
|
585
|
+
const pagesDir = path.join(this.frontendDir, "resources", "js", "pages", "auth");
|
|
586
|
+
if (!fs.existsSync(pagesDir)) fs.mkdirSync(pagesDir, { recursive: true });
|
|
587
|
+
|
|
588
|
+
const loginContent = `import { useState } from 'react';
|
|
589
|
+
import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
|
|
590
|
+
import { Link } from 'react-router-dom';
|
|
591
|
+
import Card from '../../blue-bird/components/Card.jsx';
|
|
592
|
+
import Input from '../../blue-bird/components/Input.jsx';
|
|
593
|
+
import Button from '../../blue-bird/components/Button.jsx';
|
|
594
|
+
import Typography from '../../blue-bird/components/Typography.jsx';
|
|
595
|
+
|
|
596
|
+
export default function Login() {
|
|
597
|
+
const { t, lang, setLang } = useLanguage();
|
|
598
|
+
const [email, setEmail] = useState('');
|
|
599
|
+
const [password, setPassword] = useState('');
|
|
600
|
+
const [error, setError] = useState(null);
|
|
601
|
+
|
|
602
|
+
const handleSubmit = async (e) => {
|
|
603
|
+
e.preventDefault();
|
|
604
|
+
const lang = localStorage.getItem("lila_lang") ?? "en";
|
|
605
|
+
try {
|
|
606
|
+
const res = await fetch('/auth/login', {
|
|
607
|
+
method: 'POST',
|
|
608
|
+
headers: { 'Content-Type': 'application/json' },
|
|
609
|
+
body: JSON.stringify({ email, password , lang })
|
|
610
|
+
});
|
|
611
|
+
const data = await res.json();
|
|
612
|
+
if (!res.ok) {
|
|
613
|
+
if (data.deleted_account_error) throw new Error(t('deleted_account_error'));
|
|
614
|
+
throw new Error(t('error_login') || t('error_general') || data.message);
|
|
615
|
+
}
|
|
616
|
+
window.location.href = '/dashboard';
|
|
617
|
+
} catch (err) {
|
|
618
|
+
setError(err.message);
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
|
|
622
|
+
return (
|
|
623
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
|
|
624
|
+
<Card className="w-full max-w-md">
|
|
625
|
+
<div className="mb-6 text-center">
|
|
626
|
+
<Typography variant="h3">{t('login')}</Typography>
|
|
627
|
+
</div>
|
|
628
|
+
{error && <div className="bg-red-100 text-red-700 p-3 mb-4 rounded-md text-sm">{error}</div>}
|
|
629
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
630
|
+
<Input label={t('email')} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
|
631
|
+
<Input label={t('password')} type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
|
632
|
+
<Button type="submit" className="w-full mt-2">{t('submit')}</Button>
|
|
633
|
+
</form>
|
|
634
|
+
<div className="mt-6 flex flex-col space-y-2 text-center text-sm">
|
|
635
|
+
<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>
|
|
636
|
+
<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>
|
|
637
|
+
</div>
|
|
638
|
+
<div className="mt-6 flex justify-center space-x-4 text-sm border-t dark:border-slate-800 pt-4">
|
|
639
|
+
<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>
|
|
640
|
+
<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>
|
|
641
|
+
</div>
|
|
642
|
+
</Card>
|
|
643
|
+
</div>
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
`;
|
|
647
|
+
fs.writeFileSync(path.join(pagesDir, "Login.jsx"), loginContent, "utf-8");
|
|
648
|
+
|
|
649
|
+
const registerContent = `import { useState } from 'react';
|
|
650
|
+
import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
|
|
651
|
+
import { Link } from 'react-router-dom';
|
|
652
|
+
import Card from '../../blue-bird/components/Card.jsx';
|
|
653
|
+
import Input from '../../blue-bird/components/Input.jsx';
|
|
654
|
+
import Button from '../../blue-bird/components/Button.jsx';
|
|
655
|
+
import Typography from '../../blue-bird/components/Typography.jsx';
|
|
656
|
+
|
|
657
|
+
export default function Register() {
|
|
658
|
+
const { t, lang, setLang } = useLanguage();
|
|
659
|
+
const [name, setName] = useState('');
|
|
660
|
+
const [email, setEmail] = useState('');
|
|
661
|
+
const [password, setPassword] = useState('');
|
|
662
|
+
const [password_confirmation, setPasswordConfirmation] = useState('');
|
|
663
|
+
const [error, setError] = useState(null);
|
|
664
|
+
|
|
665
|
+
const handleSubmit = async (e) => {
|
|
666
|
+
e.preventDefault();
|
|
667
|
+
const lang = localStorage.getItem("lila_lang") ?? "en";
|
|
668
|
+
try {
|
|
669
|
+
const res = await fetch('/auth/register', {
|
|
670
|
+
method: 'POST',
|
|
671
|
+
headers: { 'Content-Type': 'application/json' },
|
|
672
|
+
body: JSON.stringify({ name, email, password, password_confirmation, lang })
|
|
673
|
+
});
|
|
674
|
+
const data = await res.json();
|
|
675
|
+
if (!res.ok) {
|
|
676
|
+
if (data.password_confirmation_error) throw new Error(t('password_confirmation_err'));
|
|
677
|
+
if (data.deleted_account_error) throw new Error(t('deleted_account_error'));
|
|
678
|
+
if (data.error_email_register) throw new Error(t('error_email_register'));
|
|
679
|
+
throw new Error(data.message || t('error_general'));
|
|
680
|
+
}
|
|
681
|
+
window.location.href = '/login';
|
|
682
|
+
} catch (err) {
|
|
683
|
+
setError(err.message);
|
|
684
|
+
}
|
|
685
|
+
};
|
|
686
|
+
|
|
687
|
+
return (
|
|
688
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
|
|
689
|
+
<Card className="w-full max-w-md">
|
|
690
|
+
<div className="mb-6 text-center">
|
|
691
|
+
<Typography variant="h3">{t('register')}</Typography>
|
|
692
|
+
</div>
|
|
693
|
+
{error && <div className="bg-red-100 text-red-700 p-3 mb-4 rounded-md text-sm">{error}</div>}
|
|
694
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
695
|
+
<Input label={t('name')} type="text" value={name} onChange={(e) => setName(e.target.value)} required />
|
|
696
|
+
<Input label={t('email')} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
|
697
|
+
<Input label={t('password')} type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
|
698
|
+
<Input label={t('password_confirmation') || 'Confirm Password'} type="password" value={password_confirmation} onChange={(e) => setPasswordConfirmation(e.target.value)} required />
|
|
699
|
+
|
|
700
|
+
<Button type="submit" className="w-full mt-2">{t('submit')}</Button>
|
|
701
|
+
</form>
|
|
702
|
+
<div className="mt-6 text-center text-sm">
|
|
703
|
+
<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>
|
|
704
|
+
</div>
|
|
705
|
+
<div className="mt-6 flex justify-center space-x-4 text-sm border-t dark:border-slate-800 pt-4">
|
|
706
|
+
<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>
|
|
707
|
+
<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>
|
|
708
|
+
</div>
|
|
709
|
+
</Card>
|
|
710
|
+
</div>
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
`;
|
|
714
|
+
fs.writeFileSync(path.join(pagesDir, "Register.jsx"), registerContent, "utf-8");
|
|
715
|
+
|
|
716
|
+
const forgotPasswordContent = `import { useState } from 'react';
|
|
717
|
+
import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
|
|
718
|
+
import { Link } from 'react-router-dom';
|
|
719
|
+
import Card from '../../blue-bird/components/Card.jsx';
|
|
720
|
+
import Input from '../../blue-bird/components/Input.jsx';
|
|
721
|
+
import Button from '../../blue-bird/components/Button.jsx';
|
|
722
|
+
import Typography from '../../blue-bird/components/Typography.jsx';
|
|
723
|
+
|
|
724
|
+
export default function ForgotPassword() {
|
|
725
|
+
const { t, lang, setLang } = useLanguage();
|
|
726
|
+
const [email, setEmail] = useState('');
|
|
727
|
+
const [message, setMessage] = useState(null);
|
|
728
|
+
const [error, setError] = useState(null);
|
|
729
|
+
|
|
730
|
+
const handleSubmit = async (e) => {
|
|
731
|
+
e.preventDefault();
|
|
732
|
+
setMessage(null);
|
|
733
|
+
setError(null);
|
|
734
|
+
try {
|
|
735
|
+
const res = await fetch('/auth/forgot-password', {
|
|
736
|
+
method: 'POST',
|
|
737
|
+
headers: { 'Content-Type': 'application/json' },
|
|
738
|
+
body: JSON.stringify({ email })
|
|
739
|
+
});
|
|
740
|
+
const data = await res.json();
|
|
741
|
+
if (!res.ok) throw new Error(data.message || t('error_general'));
|
|
742
|
+
setMessage(t('If the email is valid, a password reset link has been sent') || data.message);
|
|
743
|
+
} catch (err) {
|
|
744
|
+
setError(err.message);
|
|
745
|
+
}
|
|
746
|
+
};
|
|
747
|
+
|
|
748
|
+
return (
|
|
749
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
|
|
750
|
+
<Card className="w-full max-w-md">
|
|
751
|
+
<div className="mb-4 text-center">
|
|
752
|
+
<Typography variant="h3">{t('forgot_password') || 'Forgot Password'}</Typography>
|
|
753
|
+
</div>
|
|
754
|
+
<div className="mb-6 text-center">
|
|
755
|
+
<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>
|
|
756
|
+
</div>
|
|
757
|
+
{error && <div className="bg-red-100 text-red-700 p-3 mb-4 rounded-md text-sm">{error}</div>}
|
|
758
|
+
{message && <div className="bg-green-100 text-green-700 p-3 mb-4 rounded-md text-sm">{message}</div>}
|
|
759
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
760
|
+
<Input label={t('email')} type="email" value={email} onChange={(e) => setEmail(e.target.value)} required />
|
|
761
|
+
<Button type="submit" className="w-full mt-2">{t('submit')}</Button>
|
|
762
|
+
</form>
|
|
763
|
+
<div className="mt-6 text-center text-sm">
|
|
764
|
+
<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>
|
|
765
|
+
</div>
|
|
766
|
+
</Card>
|
|
767
|
+
</div>
|
|
768
|
+
);
|
|
769
|
+
}
|
|
770
|
+
`;
|
|
771
|
+
fs.writeFileSync(path.join(pagesDir, "ForgotPassword.jsx"), forgotPasswordContent, "utf-8");
|
|
772
|
+
|
|
773
|
+
const resetPasswordContent = `import { useState, useEffect } from 'react';
|
|
774
|
+
import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
|
|
775
|
+
import { useLocation, Link } from 'react-router-dom';
|
|
776
|
+
import Card from '../../blue-bird/components/Card.jsx';
|
|
777
|
+
import Input from '../../blue-bird/components/Input.jsx';
|
|
778
|
+
import Button from '../../blue-bird/components/Button.jsx';
|
|
779
|
+
import Typography from '../../blue-bird/components/Typography.jsx';
|
|
780
|
+
|
|
781
|
+
export default function ResetPassword() {
|
|
782
|
+
const { t, lang, setLang } = useLanguage();
|
|
783
|
+
const [password, setPassword] = useState('');
|
|
784
|
+
const [password_confirmation, setPasswordConfirmation] = useState('');
|
|
785
|
+
const [token, setToken] = useState('');
|
|
786
|
+
const [message, setMessage] = useState(null);
|
|
787
|
+
const [error, setError] = useState(null);
|
|
788
|
+
|
|
789
|
+
const search = useLocation().search;
|
|
790
|
+
useEffect(() => {
|
|
791
|
+
const urlParams = new URLSearchParams(search);
|
|
792
|
+
const t = urlParams.get('token');
|
|
793
|
+
if (t) setToken(t);
|
|
794
|
+
}, [search]);
|
|
795
|
+
|
|
796
|
+
useEffect(() => {
|
|
797
|
+
if (token) {
|
|
798
|
+
const fetchUser = async () => {
|
|
799
|
+
const res = await fetch('/auth/reset-password/validate', {
|
|
800
|
+
method: 'POST',
|
|
801
|
+
headers: { 'Content-Type': 'application/json' },
|
|
802
|
+
body: JSON.stringify({ token })
|
|
803
|
+
});
|
|
804
|
+
if (!res.ok) return window.location.href = '/login';
|
|
805
|
+
const data = await res.json();
|
|
806
|
+
setUser(data.user);
|
|
807
|
+
};
|
|
808
|
+
fetchUser();
|
|
809
|
+
}
|
|
810
|
+
}, [token]);
|
|
811
|
+
|
|
812
|
+
const handleSubmit = async (e) => {
|
|
813
|
+
e.preventDefault();
|
|
814
|
+
setMessage(null);
|
|
815
|
+
setError(null);
|
|
816
|
+
try {
|
|
817
|
+
const res = await fetch('/auth/reset-password', {
|
|
818
|
+
method: 'POST',
|
|
819
|
+
headers: { 'Content-Type': 'application/json' },
|
|
820
|
+
body: JSON.stringify({ token, password, password_confirmation })
|
|
821
|
+
});
|
|
822
|
+
const data = await res.json();
|
|
823
|
+
if (!res.ok) {
|
|
824
|
+
if (data.password_confirmation) throw new Error(t('password_confirmation_err'));
|
|
825
|
+
if (data.token) throw new Error(t('error_token_reset') || data.message);
|
|
826
|
+
throw new Error(data.message || t('error_general'));
|
|
827
|
+
}
|
|
828
|
+
setMessage(data.message);
|
|
829
|
+
setTimeout(() => { window.location.href = '/login'; }, 2000);
|
|
830
|
+
} catch (err) {
|
|
831
|
+
setError(err.message);
|
|
832
|
+
}
|
|
833
|
+
};
|
|
834
|
+
|
|
835
|
+
return (
|
|
836
|
+
<div className="min-h-screen flex flex-col items-center justify-center bg-slate-50 dark:bg-slate-900 p-4">
|
|
837
|
+
<Card className="w-full max-w-md">
|
|
838
|
+
<div className="mb-6 text-center">
|
|
839
|
+
<Typography variant="h3">Reset Password</Typography>
|
|
840
|
+
</div>
|
|
841
|
+
{error && <div className="bg-red-100 text-red-700 p-3 mb-4 rounded-md text-sm">{error}</div>}
|
|
842
|
+
{message && <div className="bg-green-100 text-green-700 p-3 mb-4 rounded-md text-sm">{message}</div>}
|
|
843
|
+
<form onSubmit={handleSubmit} className="space-y-4">
|
|
844
|
+
<input type="hidden" value={token} required />
|
|
845
|
+
<Input label={'New ' + (t('password') || 'Password')} type="password" value={password} onChange={(e) => setPassword(e.target.value)} required />
|
|
846
|
+
<Input label={t('password_confirmation') || 'Confirm Password'} type="password" value={password_confirmation} onChange={(e) => setPasswordConfirmation(e.target.value)} required />
|
|
847
|
+
<Button type="submit" className="w-full mt-2">{t('submit')}</Button>
|
|
848
|
+
</form>
|
|
849
|
+
<div className="mt-6 text-center text-sm">
|
|
850
|
+
<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>
|
|
851
|
+
</div>
|
|
852
|
+
</Card>
|
|
853
|
+
</div>
|
|
854
|
+
);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
`;
|
|
858
|
+
fs.writeFileSync(path.join(pagesDir, "ResetPassword.jsx"), resetPasswordContent, "utf-8");
|
|
859
|
+
|
|
860
|
+
const dashboardContent = ` import { useLanguage } from '../../blue-bird/contexts/LanguageContext.jsx';
|
|
861
|
+
import { useEffect, useState } from 'react';
|
|
862
|
+
import Button from '../../blue-bird/components/Button.jsx';
|
|
863
|
+
import Typography from '../../blue-bird/components/Typography.jsx';
|
|
864
|
+
import Card from '../../blue-bird/components/Card.jsx';
|
|
865
|
+
|
|
866
|
+
export default function Dashboard() {
|
|
867
|
+
const { t, lang, setLang } = useLanguage();
|
|
868
|
+
const [user, setUser] = useState(null);
|
|
869
|
+
|
|
870
|
+
useEffect(() => {
|
|
871
|
+
const fetchUser = async () => {
|
|
872
|
+
const res = await fetch('/auth/validate');
|
|
873
|
+
if (!res.ok) return window.location.href = '/login';
|
|
874
|
+
const data = await res.json();
|
|
875
|
+
setUser(data.user);
|
|
876
|
+
};
|
|
877
|
+
fetchUser();
|
|
878
|
+
}, []);
|
|
879
|
+
|
|
880
|
+
const logout = async () => {
|
|
881
|
+
const res = await fetch('/auth/logout');
|
|
882
|
+
if (!res.ok) throw new Error(t('error_general'));
|
|
883
|
+
return window.location.href = '/login';
|
|
884
|
+
};
|
|
885
|
+
|
|
886
|
+
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>;
|
|
887
|
+
|
|
888
|
+
return (
|
|
889
|
+
<div className="min-h-screen bg-slate-50 dark:bg-slate-900 text-slate-900 dark:text-slate-100 flex flex-col">
|
|
890
|
+
<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">
|
|
891
|
+
<Typography variant="h4">{t('dashboard')}</Typography>
|
|
892
|
+
<div className="flex items-center space-x-4">
|
|
893
|
+
<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>
|
|
894
|
+
<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>
|
|
895
|
+
<div className="w-px h-4 bg-slate-200 dark:bg-slate-700 mx-2"></div>
|
|
896
|
+
<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>
|
|
897
|
+
</div>
|
|
898
|
+
</header>
|
|
899
|
+
<main className="flex-1 p-8 max-w-7xl mx-auto w-full">
|
|
900
|
+
<Card>
|
|
901
|
+
<Typography variant="h3" className="mb-2">Welcome, {user.email}!</Typography>
|
|
902
|
+
<Typography variant="muted">You are successfully logged into your dashboard.</Typography>
|
|
903
|
+
</Card>
|
|
904
|
+
</main>
|
|
905
|
+
</div>
|
|
906
|
+
);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
`;
|
|
910
|
+
const authenticatedPagesDir = path.join(this.frontendDir, "resources", "js", "pages", "authenticated");
|
|
911
|
+
if (!fs.existsSync(authenticatedPagesDir)) fs.mkdirSync(authenticatedPagesDir, { recursive: true });
|
|
912
|
+
fs.writeFileSync(path.join(authenticatedPagesDir, "Dashboard.jsx"), dashboardContent, "utf-8");
|
|
913
|
+
|
|
914
|
+
console.log(chalk.green("✓ Frontend React UI Components generated."));
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
modifyEntryFiles() {
|
|
918
|
+
const backendIndexFile = path.join(this.backendDir, "index.js");
|
|
919
|
+
if (fs.existsSync(backendIndexFile)) {
|
|
920
|
+
let backendIndex = fs.readFileSync(backendIndexFile, "utf-8");
|
|
921
|
+
if (!backendIndex.includes("routerAuth")) {
|
|
922
|
+
backendIndex = backendIndex.replace(
|
|
923
|
+
'import routerFrontendExample from "./routes/frontend.js";',
|
|
924
|
+
'import routerFrontendExample from "./routes/frontend.js";\nimport routerAuth from "./routes/auth.js";\nimport routerAuthenticated from "./routes/authenticated.js";'
|
|
925
|
+
);
|
|
926
|
+
backendIndex = backendIndex.replace(
|
|
927
|
+
'routes: [routerApiExample, routerFrontendExample]',
|
|
928
|
+
'routes: [routerApiExample, routerAuth, routerAuthenticated, routerFrontendExample]'
|
|
929
|
+
);
|
|
930
|
+
fs.writeFileSync(backendIndexFile, backendIndex, "utf-8");
|
|
931
|
+
console.log(chalk.green("✓ backend/index.js updated to include new routes."));
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
const appJsxFile = path.join(this.frontendDir, "resources", "js", "App.jsx");
|
|
936
|
+
if (fs.existsSync(appJsxFile)) {
|
|
937
|
+
let appJsx = fs.readFileSync(appJsxFile, "utf-8");
|
|
938
|
+
if (!appJsx.includes("LanguageProvider")) {
|
|
939
|
+
appJsx = appJsx.replace(
|
|
940
|
+
"import Home from './pages/Home';",
|
|
941
|
+
"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';"
|
|
942
|
+
);
|
|
943
|
+
appJsx = appJsx.replace(
|
|
944
|
+
"<Router>",
|
|
945
|
+
"<LanguageProvider>\n <Router>"
|
|
946
|
+
);
|
|
947
|
+
appJsx = appJsx.replace(
|
|
948
|
+
"</Router>",
|
|
949
|
+
"</Router>\n </LanguageProvider>"
|
|
950
|
+
);
|
|
951
|
+
appJsx = appJsx.replace(
|
|
952
|
+
'<Route path="/about" element={<About />} />',
|
|
953
|
+
'<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 />} />'
|
|
954
|
+
);
|
|
955
|
+
fs.writeFileSync(appJsxFile, appJsx, "utf-8");
|
|
956
|
+
console.log(chalk.green("✓ frontend/resources/js/App.jsx updated with routes and Provider."));
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
const initializer = new ScaffoldingAuth();
|
|
963
|
+
export default initializer;
|
|
964
|
+
|
|
965
|
+
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
966
|
+
initializer.run();
|
|
967
|
+
}
|