@seip/blue-bird 0.4.7 → 0.4.8
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/AGENTS.md +40 -2
- package/backend/index.js +19 -0
- package/backend/routes/api.js +44 -0
- package/backend/routes/frontend.js +11 -5
- package/core/auth.js +59 -21
- package/core/cache.js +32 -29
- package/core/config.js +1 -0
- package/frontend/resources/js/App.jsx +1 -3
- package/frontend/resources/js/blue-bird/contexts/SPAContext.jsx +5 -6
- package/frontend/resources/js/pages/About.jsx +4 -0
- package/frontend/resources/js/pages/Home.jsx +6 -1
- package/package.json +57 -57
package/AGENTS.md
CHANGED
|
@@ -123,19 +123,57 @@ routerApi.post("/users", validateUser.middleware(), (req, res) => {
|
|
|
123
123
|
|
|
124
124
|
## 4. Authentication (Auth)
|
|
125
125
|
|
|
126
|
-
The system includes built-in JWT handling. The framework
|
|
126
|
+
The system includes built-in JWT handling with AES-256-GCM encryption. The framework handles tokens via Cookies or the `Authorization` header.
|
|
127
|
+
|
|
128
|
+
### Protecting Routes
|
|
129
|
+
Use `Auth.protect()` as a middleware to secure routes.
|
|
127
130
|
|
|
128
131
|
```javascript
|
|
129
132
|
import Auth from "@seip/blue-bird/core/auth.js";
|
|
130
133
|
|
|
131
134
|
// Protect an API route (returns 401 if failed):
|
|
132
135
|
router.get("/profile", Auth.protect(), (req, res) => {
|
|
133
|
-
// The user payload is attached to req.user
|
|
136
|
+
// The decrypted user payload is attached to req.user
|
|
134
137
|
res.json({ user: req.user });
|
|
135
138
|
});
|
|
136
139
|
|
|
137
140
|
// Protect a React route (redirects to login):
|
|
138
141
|
router.get("/dashboard", Auth.protect({ redirect: "/login" }), (req, res) => { ... });
|
|
142
|
+
|
|
143
|
+
// Custom cookie key and storage key:
|
|
144
|
+
router.get("/admin", Auth.protect({
|
|
145
|
+
cookieKey: "admin_session",
|
|
146
|
+
key: "admin"
|
|
147
|
+
}), (req, res) => {
|
|
148
|
+
res.json({ admin: req.admin });
|
|
149
|
+
});
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### Login and Logout
|
|
153
|
+
The `Auth` class provides helpers to handle session management via cookies.
|
|
154
|
+
|
|
155
|
+
```javascript
|
|
156
|
+
// Login a user
|
|
157
|
+
router.post("/login", async (req, res) => {
|
|
158
|
+
const user = { id: 1, name: "John" };
|
|
159
|
+
|
|
160
|
+
// Generates JWT and sets "auth" cookie (24h default)
|
|
161
|
+
await Auth.login(res, user);
|
|
162
|
+
|
|
163
|
+
res.json({ message: "Logged in" });
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Logout a user
|
|
167
|
+
router.post("/logout", async (req, res) => {
|
|
168
|
+
await Auth.logout(res);
|
|
169
|
+
res.json({ message: "Logged out" });
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// Customizing Login (key, expiration, cookie options)
|
|
173
|
+
await Auth.login(res, user, "my_session", {
|
|
174
|
+
expiresIn: "7d",
|
|
175
|
+
cookie: { httpOnly: true, secure: true }
|
|
176
|
+
});
|
|
139
177
|
```
|
|
140
178
|
|
|
141
179
|
## 5. Performance Coaching (Cache)
|
package/backend/index.js
CHANGED
|
@@ -2,12 +2,31 @@ import App from "@seip/blue-bird/core/app.js";
|
|
|
2
2
|
import routerApiExample from "./routes/api.js";
|
|
3
3
|
import routerFrontendExample from "./routes/frontend.js";
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Main entry point for the Blue Bird application.
|
|
7
|
+
* Initializes the App instance with routes, configuration, and starts the server.
|
|
8
|
+
*/
|
|
5
9
|
const app = new App({
|
|
10
|
+
/**
|
|
11
|
+
* Array of router instances to be registered in the application.
|
|
12
|
+
* Each router can have its own base path.
|
|
13
|
+
*/
|
|
6
14
|
routes: [routerApiExample, routerFrontendExample],
|
|
15
|
+
|
|
16
|
+
/** CORS configuration. If empty, uses default settings. */
|
|
7
17
|
cors: [],
|
|
18
|
+
|
|
19
|
+
/** Global middlewares to be applied before routes. */
|
|
8
20
|
middlewares: [],
|
|
21
|
+
|
|
22
|
+
/** Base host URL for the application. */
|
|
9
23
|
host: "http://localhost",
|
|
24
|
+
|
|
25
|
+
/** Server port, defaults to environment variable or fallback in config. */
|
|
10
26
|
port: process.env.PORT,
|
|
11
27
|
});
|
|
12
28
|
|
|
29
|
+
/**
|
|
30
|
+
* Starts the application server.
|
|
31
|
+
*/
|
|
13
32
|
app.run();
|
package/backend/routes/api.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import Router from "@seip/blue-bird/core/router.js";
|
|
2
2
|
import Validator from "@seip/blue-bird/core/validate.js";
|
|
3
|
+
import Cache from "@seip/blue-bird/core/cache.js";
|
|
4
|
+
import Auth from "@seip/blue-bird/core/auth.js"
|
|
3
5
|
|
|
4
6
|
const routerApiExample = new Router("/api");
|
|
5
7
|
|
|
8
|
+
/* simple test route */
|
|
6
9
|
routerApiExample.get("/users", (req, res) => {
|
|
7
10
|
const users = [
|
|
8
11
|
{
|
|
@@ -16,7 +19,10 @@ routerApiExample.get("/users", (req, res) => {
|
|
|
16
19
|
];
|
|
17
20
|
res.json(users);
|
|
18
21
|
});
|
|
22
|
+
/* End simple test route */
|
|
19
23
|
|
|
24
|
+
|
|
25
|
+
/* Validation example */
|
|
20
26
|
const loginSchema = {
|
|
21
27
|
email: { required: true, email: true },
|
|
22
28
|
password: { required: true, min: 6 },
|
|
@@ -27,5 +33,43 @@ const loginValidator = new Validator(loginSchema);
|
|
|
27
33
|
routerApiExample.post("/login", loginValidator.middleware(), (req, res) => {
|
|
28
34
|
res.json({ message: "Login successful", body: req.body });
|
|
29
35
|
});
|
|
36
|
+
/* End Validation example */
|
|
37
|
+
|
|
38
|
+
/* Cache example */
|
|
39
|
+
routerApiExample.get("/cache", Cache.middleware(), async (req, res) => {
|
|
40
|
+
//in debug =false , testing /api/cache should take 2 seconds , and after that should take 0 seconds
|
|
41
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
42
|
+
res.json({ message: "Cache successful" });
|
|
43
|
+
})
|
|
44
|
+
/* End Cache example */
|
|
45
|
+
|
|
46
|
+
/* Auth example */
|
|
47
|
+
routerApiExample.get("/auth_generate", async (req, res) => {
|
|
48
|
+
const token = await Auth.login(res, { id: 1, name: "John Doe" })
|
|
49
|
+
/*Or use
|
|
50
|
+
const token = Auth.generateToken({ id: user.id });//to generate token
|
|
51
|
+
res.cookie("auth", token,{
|
|
52
|
+
maxAge: 24 * 60 * 60 * 1000,
|
|
53
|
+
httpOnly: true,
|
|
54
|
+
secure: production,
|
|
55
|
+
sameSite: "strict",
|
|
56
|
+
path: "/",
|
|
57
|
+
});
|
|
58
|
+
*/
|
|
59
|
+
res.json({ message: "Auth successful", token });
|
|
60
|
+
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
routerApiExample.get("/auth_logout", async (req, res) => {
|
|
64
|
+
await Auth.logout(res)
|
|
65
|
+
res.json({ message: "Auth successful" });
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
routerApiExample.get("/auth_verify", Auth.protect(), (req, res) => {
|
|
69
|
+
// req.user has inject with the jwt token payload and decoded by the Auth.protect() middleware
|
|
70
|
+
const userInfo = req.user
|
|
71
|
+
res.json({ message: "Auth successful", user: userInfo });
|
|
72
|
+
})
|
|
73
|
+
/* End Auth example */
|
|
30
74
|
|
|
31
75
|
export default routerApiExample;
|
|
@@ -6,36 +6,42 @@ import seoData from "./seo.js";
|
|
|
6
6
|
const routerFrontendExample = new Router();
|
|
7
7
|
routerFrontendExample.use(App.helmet()); // Helmet for frontend router
|
|
8
8
|
|
|
9
|
+
/* Render HTML frontend/landing.html */
|
|
9
10
|
routerFrontendExample.get("/landing", (req, res) => {
|
|
10
11
|
return Template.renderHtml(res, "landing", {
|
|
11
|
-
metaTags:{
|
|
12
|
-
titleMeta:"Landing Example",
|
|
12
|
+
metaTags: {
|
|
13
|
+
titleMeta: "Landing Example",
|
|
13
14
|
descriptionMeta: "Description meta",
|
|
14
15
|
keywordsMeta: "keywordsMeta"
|
|
15
16
|
}
|
|
16
17
|
});
|
|
17
18
|
});
|
|
19
|
+
/* End Render HTML frontend/landing.html */
|
|
18
20
|
|
|
21
|
+
/* SEO example */
|
|
19
22
|
routerFrontendExample.seo(
|
|
20
23
|
[
|
|
21
24
|
{
|
|
22
25
|
path: "/",
|
|
23
26
|
component: "Home",
|
|
24
|
-
seoKey: "home"
|
|
25
|
-
props: { id: 1, name: "Name" }
|
|
27
|
+
seoKey: "home",//key in seo.js data
|
|
28
|
+
props: { id: 1, name: "Name" },// Props pass to react component or
|
|
26
29
|
},
|
|
27
30
|
{
|
|
28
31
|
path: "/about",
|
|
29
32
|
component: "About",
|
|
30
|
-
seoKey: "about"
|
|
33
|
+
seoKey: "about",//key in seo.js data
|
|
31
34
|
props: { id: 2, name: "Name 2" },
|
|
32
35
|
},
|
|
33
36
|
],
|
|
34
37
|
{ languages: ["en", "es"], defaultLanguage: "en", seoData },
|
|
35
38
|
);
|
|
39
|
+
/* End SEO example */
|
|
36
40
|
|
|
41
|
+
/* Render React example for '*' route (catch all routes) */
|
|
37
42
|
routerFrontendExample.get("*", (req, res) => {
|
|
38
43
|
return Template.renderReact(res, "App");
|
|
39
44
|
});
|
|
45
|
+
/* End Render React example */
|
|
40
46
|
|
|
41
47
|
export default routerFrontendExample;
|
package/core/auth.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import jwt from "jsonwebtoken";
|
|
2
2
|
import crypto from "node:crypto";
|
|
3
|
+
import Config from "./config.js";
|
|
3
4
|
|
|
5
|
+
const propsConfig = Config.props();
|
|
6
|
+
const jwtSecret = propsConfig.jwtSecret;
|
|
7
|
+
const production = propsConfig.debug;
|
|
4
8
|
/**
|
|
5
9
|
* Auth class to handle JWT generation, verification and protection with AES-256-GCM encryption.
|
|
6
10
|
*/
|
|
@@ -49,13 +53,13 @@ class Auth {
|
|
|
49
53
|
/**
|
|
50
54
|
* Generates an encrypted JWT token.
|
|
51
55
|
* @param {Object} payload - The data to store in the token.
|
|
52
|
-
* @param {string} [secret=process.env.JWT_SECRET] - The secret key.
|
|
56
|
+
* @param {string} [secret=process.env.JWT_SECRET] - The secret key .
|
|
53
57
|
* @param {string} [expiresIn="24h"] - Expiration time.
|
|
54
58
|
* @returns {string} The generated token.
|
|
55
59
|
*/
|
|
56
60
|
static generateToken(
|
|
57
61
|
payload,
|
|
58
|
-
secret =
|
|
62
|
+
secret = jwtSecret,
|
|
59
63
|
expiresIn = "24h"
|
|
60
64
|
) {
|
|
61
65
|
if (!secret)
|
|
@@ -70,7 +74,7 @@ class Auth {
|
|
|
70
74
|
* @param {string} [secret=process.env.JWT_SECRET] - The secret key.
|
|
71
75
|
* @returns {Object|null} The decoded and decrypted payload or null if invalid.
|
|
72
76
|
*/
|
|
73
|
-
static verifyToken(token, secret =
|
|
77
|
+
static verifyToken(token, secret = jwtSecret) {
|
|
74
78
|
if (!secret)
|
|
75
79
|
throw new Error("FATAL: JWT_SECRET environment variable is not defined.");
|
|
76
80
|
try {
|
|
@@ -84,30 +88,42 @@ class Auth {
|
|
|
84
88
|
|
|
85
89
|
/**
|
|
86
90
|
* Middleware to protect routes. Checks for token in Cookies or Authorization header.
|
|
87
|
-
* @param {Object} options - Options for protection.
|
|
91
|
+
* @param {Object} [options={}] - Options for protection.
|
|
88
92
|
* @param {string} [options.redirect=null] - URL to redirect if not authenticated.
|
|
89
93
|
* @param {string} [options.key="user"] - Key to store the decoded token in the request.
|
|
94
|
+
* @param {string} [options.cookieKey="auth"] - The cookie key to look for the token.
|
|
90
95
|
* @returns {Function} Express middleware.
|
|
96
|
+
* @example
|
|
97
|
+
* router.get("/profile", Auth.protect(), (req, res) => { ... });
|
|
98
|
+
* // Or with custom cookie key:
|
|
99
|
+
* router.get("/admin", Auth.protect({ cookieKey: "admin_session" }), (req, res) => { ... });
|
|
91
100
|
*/
|
|
92
|
-
static protect(options = {
|
|
101
|
+
static protect(options = {}) {
|
|
102
|
+
const { redirect = null, key = "user", cookieKey = "auth" } = options;
|
|
103
|
+
|
|
93
104
|
return (req, res, next) => {
|
|
94
105
|
const token =
|
|
95
|
-
req.cookies?.
|
|
106
|
+
req.cookies?.[cookieKey] || req.headers.authorization?.split(" ")[1];
|
|
96
107
|
|
|
97
|
-
const isContentTypeJson =
|
|
108
|
+
const isContentTypeJson =
|
|
109
|
+
req.headers["content-type"] === "application/json";
|
|
98
110
|
|
|
99
111
|
if (!token) {
|
|
100
|
-
if (
|
|
101
|
-
return isContentTypeJson
|
|
112
|
+
if (redirect && !isContentTypeJson) return res.redirect(redirect);
|
|
113
|
+
return isContentTypeJson
|
|
114
|
+
? res.status(401).json({ message: "Unauthorized" })
|
|
115
|
+
: res.status(401).send();
|
|
102
116
|
}
|
|
103
117
|
|
|
104
118
|
const decoded = this.verifyToken(token);
|
|
105
119
|
if (!decoded) {
|
|
106
|
-
if (
|
|
107
|
-
return isContentTypeJson
|
|
120
|
+
if (redirect && !isContentTypeJson) return res.redirect(redirect);
|
|
121
|
+
return isContentTypeJson
|
|
122
|
+
? res.status(401).json({ message: "Unauthorized" })
|
|
123
|
+
: res.status(401).send();
|
|
108
124
|
}
|
|
109
125
|
|
|
110
|
-
req[
|
|
126
|
+
req[key || "user"] = decoded;
|
|
111
127
|
next();
|
|
112
128
|
};
|
|
113
129
|
}
|
|
@@ -117,12 +133,29 @@ class Auth {
|
|
|
117
133
|
* @param {import('express').Response} res - The response object.
|
|
118
134
|
* @param {Object} data - The data to store in the token.
|
|
119
135
|
* @param {string} [key="auth"] - The key for the cookie.
|
|
120
|
-
* @param {Object} [options={
|
|
121
|
-
* @
|
|
136
|
+
* @param {Object} [options={}] - Options for the cookie and token.
|
|
137
|
+
* @param {string} [options.expiresIn="24h"] - Token expiration (e.g., "1h", "7d").
|
|
138
|
+
* @param {import('express').CookieOptions} [options.cookie] - Express cookie options.
|
|
139
|
+
* @returns {Promise<string>} The generated token.
|
|
140
|
+
* @example
|
|
141
|
+
* await Auth.login(res, { id: 1, name: "Admin" });
|
|
122
142
|
*/
|
|
123
|
-
static login(res, data, key = "auth", options = {
|
|
124
|
-
const
|
|
125
|
-
|
|
143
|
+
static async login(res, data, key = "auth", options = {}) {
|
|
144
|
+
const { expiresIn = "24h", cookie = {} } = options;
|
|
145
|
+
|
|
146
|
+
const token = this.generateToken(data, jwtSecret, expiresIn);
|
|
147
|
+
|
|
148
|
+
const defaultCookieOptions = {
|
|
149
|
+
maxAge: 24 * 60 * 60 * 1000,
|
|
150
|
+
httpOnly: true,
|
|
151
|
+
secure: production,
|
|
152
|
+
sameSite: "strict",
|
|
153
|
+
path: "/",
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
const finalCookieOptions = { ...defaultCookieOptions, ...cookie };
|
|
157
|
+
|
|
158
|
+
res.cookie(key, token, finalCookieOptions);
|
|
126
159
|
return token;
|
|
127
160
|
}
|
|
128
161
|
|
|
@@ -130,11 +163,16 @@ class Auth {
|
|
|
130
163
|
* Logs out a user by clearing the authentication cookie.
|
|
131
164
|
* @param {import('express').Response} res - The response object.
|
|
132
165
|
* @param {string} [key="auth"] - The key for the cookie.
|
|
133
|
-
* @param {
|
|
134
|
-
* @returns {boolean} True if the cookie was cleared successfully.
|
|
166
|
+
* @param {import('express').CookieOptions} [options={}] - Options for clearing the cookie.
|
|
167
|
+
* @returns {Promise<boolean>} True if the cookie was cleared successfully.
|
|
168
|
+
* @example
|
|
169
|
+
* await Auth.logout(res);
|
|
135
170
|
*/
|
|
136
|
-
static logout(res, key = "auth", options = {
|
|
137
|
-
|
|
171
|
+
static async logout(res, key = "auth", options = {}) {
|
|
172
|
+
const defaultOptions = {
|
|
173
|
+
path: "/",
|
|
174
|
+
};
|
|
175
|
+
res.clearCookie(key, { ...defaultOptions, ...options });
|
|
138
176
|
return true;
|
|
139
177
|
}
|
|
140
178
|
}
|
package/core/cache.js
CHANGED
|
@@ -9,37 +9,40 @@ setInterval(() => {
|
|
|
9
9
|
}
|
|
10
10
|
}, 300000).unref();
|
|
11
11
|
/**
|
|
12
|
-
* Cache
|
|
13
|
-
*
|
|
14
|
-
|
|
15
|
-
Cache.middleware(120),
|
|
16
|
-
controller.stats
|
|
17
|
-
);
|
|
18
|
-
* */
|
|
12
|
+
* Simple in-memory Cache class to provide middleware for Express routes.
|
|
13
|
+
* Caches JSON responses based on the request URL.
|
|
14
|
+
*/
|
|
19
15
|
class Cache {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Middleware to cache responses.
|
|
18
|
+
* @param {number} [seconds=60] - Number of seconds to cache the response.
|
|
19
|
+
* @returns {Function} Express middleware function.
|
|
20
|
+
* @example
|
|
21
|
+
* router.get("/stats", Cache.middleware(120), (req, res) => {
|
|
22
|
+
* res.json({ ok: true });
|
|
23
|
+
* });
|
|
24
|
+
*/
|
|
25
|
+
static middleware(seconds = 60) {
|
|
26
|
+
return (req, res, next) => {
|
|
27
|
+
const key = req.originalUrl;
|
|
28
|
+
|
|
29
|
+
if (CACHE[key] && CACHE[key].expiry > Date.now()) {
|
|
30
|
+
return res.json(CACHE[key].data);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const originalJson = res.json.bind(res);
|
|
34
|
+
|
|
35
|
+
res.json = (body) => {
|
|
36
|
+
CACHE[key] = {
|
|
37
|
+
data: body,
|
|
38
|
+
expiry: Date.now() + seconds * 1000,
|
|
41
39
|
};
|
|
42
|
-
|
|
40
|
+
return originalJson(body);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
next();
|
|
44
|
+
};
|
|
45
|
+
}
|
|
43
46
|
}
|
|
44
47
|
|
|
45
48
|
export default Cache;
|
package/core/config.js
CHANGED
|
@@ -40,6 +40,7 @@ class Config {
|
|
|
40
40
|
host: process.env.HOST || "http://localhost",
|
|
41
41
|
appUrl: process.env.APP_URL || process.env.HOST || "http://localhost",
|
|
42
42
|
port: Number.isNaN(portRaw) ? 3000 : portRaw,
|
|
43
|
+
jwtSecret: process.env.JWT_SECRET,
|
|
43
44
|
static: {
|
|
44
45
|
path: process.env.STATIC_PATH || "frontend/public",
|
|
45
46
|
options: {},
|
|
@@ -30,12 +30,10 @@ function generateRoutes(routes, languages = []) {
|
|
|
30
30
|
const allRoutes = [];
|
|
31
31
|
|
|
32
32
|
routes.forEach(({ path: routePath, element }) => {
|
|
33
|
-
// Base path
|
|
34
33
|
allRoutes.push(
|
|
35
34
|
<Route key={routePath} path={routePath} element={element} />
|
|
36
35
|
);
|
|
37
36
|
|
|
38
|
-
// Language-prefixed paths (only if multiple languages are used)
|
|
39
37
|
if (languages && languages.length > 0) {
|
|
40
38
|
languages.forEach((lang) => {
|
|
41
39
|
const langPath = `/${lang}${routePath === "/" ? "" : routePath}`;
|
|
@@ -56,7 +54,7 @@ export default function App(_props) {
|
|
|
56
54
|
<ThemeProvider>
|
|
57
55
|
<LanguageProvider initialLang={lang}>
|
|
58
56
|
<Router>
|
|
59
|
-
<SPAProvider languages={LANGUAGES} defaultLanguage={DEFAULT_LANGUAGE}>
|
|
57
|
+
<SPAProvider languages={LANGUAGES} defaultLanguage={DEFAULT_LANGUAGE} {..._props} >
|
|
60
58
|
<Suspense fallback={<Skeleton />}>
|
|
61
59
|
<Routes>
|
|
62
60
|
{generateRoutes(ROUTES, LANGUAGES)}
|
|
@@ -28,7 +28,7 @@ const SPAContext = createContext({
|
|
|
28
28
|
* @param {Array<string>} [props.languages=[]] - Supported language codes.
|
|
29
29
|
* @param {string} [props.defaultLanguage="en"] - Default language.
|
|
30
30
|
*/
|
|
31
|
-
export function SPAProvider({
|
|
31
|
+
export function SPAProvider({ props,
|
|
32
32
|
children,
|
|
33
33
|
languages = [],
|
|
34
34
|
defaultLanguage = "en",
|
|
@@ -36,7 +36,7 @@ export function SPAProvider({
|
|
|
36
36
|
const location = useLocation();
|
|
37
37
|
const navigate = useNavigate();
|
|
38
38
|
const { lang, setLang } = useLanguage();
|
|
39
|
-
const [pageProps, setPageProps] = useState(
|
|
39
|
+
const [pageProps, setPageProps] = useState(props);
|
|
40
40
|
const [pageMeta, setPageMeta] = useState({});
|
|
41
41
|
const [loading, setLoading] = useState(false);
|
|
42
42
|
const isFirstRender = useRef(true);
|
|
@@ -136,7 +136,6 @@ export function SPAProvider({
|
|
|
136
136
|
}
|
|
137
137
|
});
|
|
138
138
|
|
|
139
|
-
// Update OG tags
|
|
140
139
|
const ogUpdates = {
|
|
141
140
|
"og:title": meta.titleMeta,
|
|
142
141
|
"og:description": meta.descriptionMeta,
|
|
@@ -153,21 +152,21 @@ export function SPAProvider({
|
|
|
153
152
|
}, []);
|
|
154
153
|
|
|
155
154
|
useEffect(() => {
|
|
156
|
-
// Sync language from URL prefix if present
|
|
157
155
|
const urlLang = detectLangFromPath(location.pathname);
|
|
158
156
|
if (urlLang) {
|
|
159
157
|
setLang(urlLang);
|
|
160
158
|
} else if (languages.length > 0 && !isFirstRender.current) {
|
|
161
|
-
// On subsequent navigations, if prefix is missing, revert to default
|
|
162
159
|
setLang(defaultLanguage);
|
|
163
160
|
}
|
|
164
161
|
|
|
165
|
-
// Skip fetch on initial render
|
|
166
162
|
if (isFirstRender.current) {
|
|
167
163
|
isFirstRender.current = false;
|
|
168
164
|
return;
|
|
169
165
|
}
|
|
170
166
|
|
|
167
|
+
setPageProps({});
|
|
168
|
+
setPageMeta({});
|
|
169
|
+
|
|
171
170
|
if (abortRef.current) {
|
|
172
171
|
abortRef.current.abort();
|
|
173
172
|
}
|
|
@@ -4,9 +4,13 @@ import { useLanguage } from '../blue-bird/contexts/LanguageContext';
|
|
|
4
4
|
|
|
5
5
|
import Card from '../blue-bird/components/Card';
|
|
6
6
|
import Typography from '../blue-bird/components/Typography'
|
|
7
|
+
import { useSPA } from '../blue-bird/contexts/SPAContext.jsx';
|
|
7
8
|
|
|
8
9
|
export default function About() {
|
|
9
10
|
const { t } = useLanguage();
|
|
11
|
+
const { navigateToLang, pageProps, pageMeta } = useSPA();
|
|
12
|
+
console.log("pageProps", pageProps);
|
|
13
|
+
console.log("pageMeta", pageMeta);
|
|
10
14
|
return (
|
|
11
15
|
<div
|
|
12
16
|
className="bg-white dark:bg-slate-900 text-gray-900 dark:text-gray-100 min-h-screen"
|
|
@@ -3,11 +3,16 @@ import Card from '../blue-bird/components/Card';
|
|
|
3
3
|
import Header from '../components/Header';
|
|
4
4
|
import { useLanguage } from '../blue-bird/contexts/LanguageContext';
|
|
5
5
|
import Typography from '../blue-bird/components/Typography';
|
|
6
|
+
import { useSPA } from '../blue-bird/contexts/SPAContext.jsx';
|
|
6
7
|
|
|
7
8
|
export default function Home() {
|
|
8
9
|
const { t } = useLanguage();
|
|
10
|
+
const { navigateToLang, pageProps, pageMeta } = useSPA();
|
|
11
|
+
console.log("pageProps", pageProps);
|
|
12
|
+
console.log("pageMeta", pageMeta);
|
|
13
|
+
|
|
9
14
|
useEffect(() => {
|
|
10
|
-
|
|
15
|
+
|
|
11
16
|
fetch("/api/login", {
|
|
12
17
|
method: "POST",
|
|
13
18
|
headers: {
|
package/package.json
CHANGED
|
@@ -1,58 +1,58 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@seip/blue-bird",
|
|
3
|
-
"version": "0.4.
|
|
4
|
-
"description": "Express + React opinionated framework with SPA or API architecture and built-in JWT auth",
|
|
5
|
-
"type": "module",
|
|
6
|
-
"bin": {
|
|
7
|
-
"blue-bird": "core/cli/init.js"
|
|
8
|
-
},
|
|
9
|
-
"repository": {
|
|
10
|
-
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/seip25/Blue-bird.git"
|
|
12
|
-
},
|
|
13
|
-
"keywords": [
|
|
14
|
-
"express",
|
|
15
|
-
"react",
|
|
16
|
-
"framework",
|
|
17
|
-
"vite"
|
|
18
|
-
],
|
|
19
|
-
"author": "Seip25",
|
|
20
|
-
"license": "MIT",
|
|
21
|
-
"bugs": {
|
|
22
|
-
"url": "https://github.com/seip25/Blue-bird/issues"
|
|
23
|
-
},
|
|
24
|
-
"homepage": "https://seip25.github.io/Blue-bird/",
|
|
25
|
-
"scripts": {
|
|
26
|
-
"dev": "node --watch --env-file=.env backend/index.js",
|
|
27
|
-
"start": "node --env-file=.env backend/index.js",
|
|
28
|
-
"create-react-app": "node core/cli/react.js",
|
|
29
|
-
"react": "node core/cli/react.js",
|
|
30
|
-
"init": "node core/cli/init.js",
|
|
31
|
-
"route": "node core/cli/route.js",
|
|
32
|
-
"component": "node core/cli/component.js",
|
|
33
|
-
"swagger-install": "node core/cli/swagger.js",
|
|
34
|
-
"vite:dev": "vite",
|
|
35
|
-
"vite:build": "vite build"
|
|
36
|
-
},
|
|
37
|
-
"dependencies": {
|
|
38
|
-
"@tailwindcss/vite": "^4.2.2",
|
|
39
|
-
"chalk": "^5.6.2",
|
|
40
|
-
"compression": "^1.8.1",
|
|
41
|
-
"cookie-parser": "^1.4.7",
|
|
42
|
-
"cors": "^2.8.6",
|
|
43
|
-
"express": "^5.2.1",
|
|
44
|
-
"express-rate-limit": "^8.2.1",
|
|
45
|
-
"helmet": "^8.1.0",
|
|
46
|
-
"jsonwebtoken": "^9.0.2",
|
|
47
|
-
"multer": "^2.0.2",
|
|
48
|
-
"react": "^19.2.4",
|
|
49
|
-
"react-dom": "^19.2.4",
|
|
50
|
-
"react-router-dom": "^7.2.0",
|
|
51
|
-
"tailwindcss": "^4.2.2",
|
|
52
|
-
"xss": "^1.0.15"
|
|
53
|
-
},
|
|
54
|
-
"devDependencies": {
|
|
55
|
-
"@vitejs/plugin-react": "^4.3.4",
|
|
56
|
-
"vite": "^7.3.1"
|
|
57
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@seip/blue-bird",
|
|
3
|
+
"version": "0.4.8",
|
|
4
|
+
"description": "Express + React opinionated framework with SPA or API architecture and built-in JWT auth",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"blue-bird": "core/cli/init.js"
|
|
8
|
+
},
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/seip25/Blue-bird.git"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"express",
|
|
15
|
+
"react",
|
|
16
|
+
"framework",
|
|
17
|
+
"vite"
|
|
18
|
+
],
|
|
19
|
+
"author": "Seip25",
|
|
20
|
+
"license": "MIT",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/seip25/Blue-bird/issues"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://seip25.github.io/Blue-bird/",
|
|
25
|
+
"scripts": {
|
|
26
|
+
"dev": "node --watch --env-file=.env backend/index.js",
|
|
27
|
+
"start": "node --env-file=.env backend/index.js",
|
|
28
|
+
"create-react-app": "node core/cli/react.js",
|
|
29
|
+
"react": "node core/cli/react.js",
|
|
30
|
+
"init": "node core/cli/init.js",
|
|
31
|
+
"route": "node core/cli/route.js",
|
|
32
|
+
"component": "node core/cli/component.js",
|
|
33
|
+
"swagger-install": "node core/cli/swagger.js",
|
|
34
|
+
"vite:dev": "vite",
|
|
35
|
+
"vite:build": "vite build"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@tailwindcss/vite": "^4.2.2",
|
|
39
|
+
"chalk": "^5.6.2",
|
|
40
|
+
"compression": "^1.8.1",
|
|
41
|
+
"cookie-parser": "^1.4.7",
|
|
42
|
+
"cors": "^2.8.6",
|
|
43
|
+
"express": "^5.2.1",
|
|
44
|
+
"express-rate-limit": "^8.2.1",
|
|
45
|
+
"helmet": "^8.1.0",
|
|
46
|
+
"jsonwebtoken": "^9.0.2",
|
|
47
|
+
"multer": "^2.0.2",
|
|
48
|
+
"react": "^19.2.4",
|
|
49
|
+
"react-dom": "^19.2.4",
|
|
50
|
+
"react-router-dom": "^7.2.0",
|
|
51
|
+
"tailwindcss": "^4.2.2",
|
|
52
|
+
"xss": "^1.0.15"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@vitejs/plugin-react": "^4.3.4",
|
|
56
|
+
"vite": "^7.3.1"
|
|
57
|
+
}
|
|
58
58
|
}
|