easy-starter 1.2.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,13 +5,31 @@ import cors from "cors";
5
5
  import express from "express";
6
6
 
7
7
  import easyDBNode from "easy-db-node";
8
- import { renderToHTML } from "ssr-hook/server";
9
8
  import { setAPIBackend } from "typed-client-server-api";
10
9
 
10
+ // --- SSR START ---
11
+ import { renderToHTML } from "ssr-hook/server";
12
+ // --- SSR END ---
13
+
14
+ // --- ANALYTICS START ---
11
15
  import { postEasyAnalytics, getEasyAnalytics } from "easy-analytics/server";
16
+ // --- ANALYTICS END ---
12
17
 
13
- import App from "../src/App";
18
+ // --- AUTH START ---
19
+ import { EasyLoginServer } from "easy-user-auth/server";
20
+ import { sendMailForgottenPassword } from "./mail";
21
+ import { UserProfile } from "../types";
22
+ // --- AUTH END ---
14
23
 
24
+ // --- OGIMAGE START ---
25
+ import puppeteer from "puppeteer";
26
+ // --- OGIMAGE END ---
27
+
28
+ // --- RESIZER START ---
29
+ import { expressImageResizer } from "easy-image-resizer";
30
+ // --- RESIZER END ---
31
+
32
+ import App from "../src/App";
15
33
  import { API, Database } from "../types";
16
34
 
17
35
  const PORT = process.env.SERVER_PORT || 1111;
@@ -20,7 +38,7 @@ const LOCALHOST = `http://localhost:${PORT}`;
20
38
  // The ORIGIN is used for SEO and by ssr-hook to know where the requests are coming from.
21
39
  const ORIGIN = process.env.NODE_ENV === "development"
22
40
  ? LOCALHOST
23
- : process.env.SERVER_URL;
41
+ : process.env.SERVER_URL || LOCALHOST;
24
42
 
25
43
  const INDEX_HTML_PATH = resolve(__dirname, "..", "dist", "index.html");
26
44
 
@@ -36,23 +54,56 @@ const app = express();
36
54
 
37
55
  if (process.env.NODE_ENV === "development") {
38
56
  console.log("Starting development server...");
39
- app.use(cors());
57
+ app.use(cors({
58
+ origin: ["http://localhost:5173", "http://localhost:1111"],
59
+ credentials: true
60
+ }));
61
+ } else {
62
+ app.use(cors({
63
+ origin: ORIGIN,
64
+ credentials: true
65
+ }));
40
66
  }
41
67
 
42
- // ---------------------------- SSR for index ----------------------------------
43
- app.get(["/", "/index.html"], async (req, res) => {
44
- try {
45
- const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
46
- const html = await renderToHTML(ORIGIN, req.url, indexHtml, <App />);
47
- res.set("Content-Type", "text/html");
48
- res.send(html);
49
- } catch (e) {
50
- console.error("SSR error:", e);
51
- res.status(500);
52
- res.json({ message: "Internal server error" });
53
- }
68
+ // Parse incoming JSON payloads.
69
+ app.use(express.json({ limit: "1MB" }));
70
+
71
+ // --- AUTH START ---
72
+ const authServer = new EasyLoginServer<UserProfile>({
73
+ jwtSecret: process.env.JWT_SECRET || "fallback-secret-key-change-me",
74
+ secureCookies: process.env.NODE_ENV === "production",
75
+ db: {
76
+ insertUser: async (user: any) => insert("user", user),
77
+ getUserById: async (id: string) => select("user", id),
78
+ getUserByMail: async (mail: string) => {
79
+ const users = await selectArray("user");
80
+ return users.find((u: any) => u.mail.toLowerCase() === mail.toLowerCase()) || null;
81
+ },
82
+ getUserByRecoveryToken: async (token: string) => {
83
+ const users = await selectArray("user");
84
+ return users.find((u: any) => u.passwordRecovery?.token === token) || null;
85
+ },
86
+ updateUser: async (id: string, user: any) => {
87
+ await update("user", id, user);
88
+ },
89
+ },
90
+ mailSender: async (lang: string, mailTo: string, token: string) => {
91
+ await sendMailForgottenPassword(lang, mailTo, token, ORIGIN);
92
+ },
54
93
  });
55
94
 
95
+ // Register all user authentication routes (/api/login, /api/register, /api/user, etc.)
96
+ authServer.registerExpressRoutes(app);
97
+ // --- AUTH END ---
98
+
99
+ // --- RESIZER START ---
100
+ // Serve and dynamically resize images from public directory with auto-caching
101
+ app.use(expressImageResizer({
102
+ sourceDir: "./public",
103
+ cacheDir: "./cache/images",
104
+ }));
105
+ // --- RESIZER END ---
106
+
56
107
  // Serve static files generated by the bundler (JS, CSS)
57
108
  app.use(express.static("dist"));
58
109
  // Serve any public assets (like images, fonts)
@@ -60,41 +111,95 @@ app.use(express.static("public"));
60
111
  // Serve the database files (e.g. uploaded images or PDF files from easy-db-node)
61
112
  app.use("/files", express.static("easy-db-files"));
62
113
 
114
+ // ------------------------------ og:image -------------------------------------
115
+ // --- OGIMAGE START ---
116
+ app.get("/og-image.png", async (req, res) => {
117
+ const path = req.query.path as string;
118
+ if (!path) {
119
+ return res.sendFile(resolve(__dirname, "..", "public", "images", "icon.png"));
120
+ }
63
121
 
64
- // --------------------------------- SEO ---------------------------------------
65
- // Basic configuration for search engine crawlers.
66
- app.get("/robots.txt", (_, res) => {
67
- res.type("text/plain");
68
- res.send(`User-agent: *\nAllow: /\n\nSitemap: ${ORIGIN}/sitemap.xml`);
69
- });
122
+ const image = await select("og-image", path);
123
+ if (image) {
124
+ return res.redirect(302, image.file.url);
125
+ }
70
126
 
71
- // Sitemap helps search engines index your site. You should dynamically generate this
72
- // based on your application's routes and database content.
73
- app.get("/sitemap.xml", async (_, res) => {
74
- // TODO: add all pages
75
- const sitemap = [{
76
- url: ORIGIN + "/",
77
- lastModified: "2026-01-01",
78
- changeFrequency: "weekly",
79
- priority: 1,
80
- }];
127
+ try {
128
+ const now = Date.now();
129
+ const browser = await puppeteer.launch({
130
+ headless: true,
131
+ args: [
132
+ '--no-sandbox',
133
+ '--disable-setuid-sandbox',
134
+ '--disable-gpu',
135
+ '--disable-dev-shm-usage',
136
+ '--no-first-run',
137
+ '--no-zygote',
138
+ '--single-process',
139
+ ]
140
+ });
81
141
 
82
- res.set("Content-Type", "text/xml");
83
- res.send(`<?xml version="1.0" encoding="UTF-8" ?>
84
- <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${sitemap.map(s => `<url>
85
- <loc>${s.url}</loc>
86
- <lastmod>${s.lastModified}</lastmod>
87
- ${s.changeFrequency ? `<changefreq>${s.changeFrequency}</changefreq>` : ""}
88
- <priority>${s.priority}</priority>
89
- </url>`).join("\n")}</urlset>`);
90
- });
142
+ const page = await browser.newPage();
143
+ // --- SSR START ---
144
+ await page.setJavaScriptEnabled(false);
145
+ // --- SSR END ---
146
+ // --- NO_SSR START ---
147
+ await page.setJavaScriptEnabled(true);
148
+ // --- NO_SSR END ---
149
+ await page.setViewport({ width: 600, height: 500 });
91
150
 
151
+ // Render target page
152
+ await page.goto(`${LOCALHOST}${path}`, { waitUntil: 'networkidle0' });
153
+ const base64 = await page.screenshot({ type: "png", encoding: "base64" });
154
+ await browser.close();
92
155
 
93
- // This parses incoming JSON payloads for the API and analytics.
94
- app.use(express.json({ limit: "1MB" }));
156
+ await update("og-image", path, {
157
+ dateCreated: new Date().toISOString(),
158
+ file: {
159
+ type: "EASY_DB_FILE",
160
+ url: `data:image/png;base64,${base64}`,
161
+ },
162
+ } as any);
163
+
164
+ console.log(`Render og:image for ${path} took ${Date.now() - now} ms.`);
165
+
166
+ const img = await select("og-image", path);
167
+ return res.redirect(302, img!.file.url);
168
+
169
+ } catch (e) {
170
+ console.error("og:image rendering error:", e);
171
+ return res.sendFile(resolve(__dirname, "..", "public", "images", "icon.png"));
172
+ }
173
+ });
174
+ // --- OGIMAGE END ---
175
+
176
+ // ---------------------------- SSR for index ----------------------------------
177
+ // --- SSR START ---
178
+ app.get(["/", "/index.html"], async (req, res) => {
179
+ try {
180
+ const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
181
+ const html = await renderToHTML(ORIGIN, req.url, indexHtml, <App />);
182
+ res.set("Content-Type", "text/html");
183
+ res.send(html);
184
+ } catch (e) {
185
+ console.error("SSR error:", e);
186
+ res.status(500).json({ message: "Internal server error" });
187
+ }
188
+ });
189
+ // --- SSR END ---
190
+ // --- NO_SSR START ---
191
+ app.get(["/", "/index.html"], async (req, res) => {
192
+ try {
193
+ res.sendFile(INDEX_HTML_PATH);
194
+ } catch (e) {
195
+ console.error("Error serving index.html:", e);
196
+ res.status(500).json({ message: "Internal server error" });
197
+ }
198
+ });
199
+ // --- NO_SSR END ---
95
200
 
96
201
  // --------------------------- Easy analytics ----------------------------------
97
- // Endpoint to receive analytics data from the client.
202
+ // --- ANALYTICS START ---
98
203
  app.post("/api/easy-analytics", async (req, res) => {
99
204
  const userAgent = req.headers['user-agent'] || "";
100
205
  const ip = req.headers['x-forwarded-for'] || req.socket.remoteAddress || "";
@@ -107,10 +212,9 @@ app.post("/api/easy-analytics", async (req, res) => {
107
212
  res.status(400).json({ message: e.message || "Bad request" });
108
213
  }
109
214
  });
215
+ // --- ANALYTICS END ---
110
216
 
111
217
  // --------------------------------- API ---------------------------------------
112
- // Set up our type-safe API backend. All functions defined here map directly to the
113
- // API type defined in types.d.ts, and can be called from the frontend using by api., useApi., or useSSRApi.
114
218
  setAPIBackend<API>(app, {
115
219
  async getItems() {
116
220
  return await selectArray("item");
@@ -118,30 +222,130 @@ setAPIBackend<API>(app, {
118
222
  async getItem({ id }) {
119
223
  const item = await select("item", id);
120
224
  if (!item) throw new Error("Item not found");
121
- return item; // easy-db-node select returns the object
225
+ return item;
122
226
  },
123
227
  async addItem({ name, value }) {
124
- const id = await insert("item", { name, value });
228
+ const id = await insert("item", { name, value } as any);
125
229
  return id;
126
230
  },
127
231
  async removeItem({ id }) {
128
232
  await remove("item", id);
129
233
  return true;
130
234
  },
131
- // --- ADMIN START ---
132
- async getEasyAnalyticsData({ password, month }) {
235
+
236
+ /* --- ADMIN_NO_AUTH START --- */
237
+ async getEasyAnalyticsData({ password, month }: { password?: string; month: string }) {
133
238
  const validPassword = process.env.ADMIN_PASSWORD;
134
239
  if (!validPassword || password !== validPassword) {
135
240
  throw new Error("Invalid password or server error");
136
241
  }
242
+ // --- ANALYTICS START ---
137
243
  return await getEasyAnalytics(month);
138
- }
139
- // --- ADMIN END ---
244
+ // --- ANALYTICS END ---
245
+ // --- NO_ANALYTICS START ---
246
+ return [];
247
+ // --- NO_ANALYTICS END ---
248
+ },
249
+ /* --- ADMIN_NO_AUTH END --- */
250
+
251
+ // --- ADMIN_WITH_AUTH START ---
252
+ async getEasyAnalyticsData({ month }: { month: string }, req?: any, res?: any) {
253
+ const session = await authServer.checkLogin(req!, res!);
254
+ const user = await select("user", session.userId);
255
+ if (!user || user.role !== "admin") {
256
+ throw new Error("Forbidden");
257
+ }
258
+ // --- ANALYTICS START ---
259
+ return await getEasyAnalytics(month);
260
+ // --- ANALYTICS END ---
261
+ // --- NO_ANALYTICS START ---
262
+ return [];
263
+ // --- NO_ANALYTICS END ---
264
+ },
265
+ // --- ADMIN_WITH_AUTH END ---
266
+
267
+ /* --- ADMIN_ERRORS_NO_AUTH START --- */
268
+ async getEasyAnalyticsErrors({ password, month }: { password?: string; month: string }) {
269
+ const validPassword = process.env.ADMIN_PASSWORD;
270
+ if (!validPassword || password !== validPassword) {
271
+ throw new Error("Invalid password or server error");
272
+ }
273
+ return await selectArray(`easyAnalyticsError-${month}` as any);
274
+ },
275
+ /* --- ADMIN_ERRORS_NO_AUTH END --- */
276
+
277
+ // --- ADMIN_ERRORS_WITH_AUTH START ---
278
+ async getEasyAnalyticsErrors({ month }: { month: string }, req?: any, res?: any) {
279
+ const session = await authServer.checkLogin(req!, res!);
280
+ const user = await select("user", session.userId);
281
+ if (!user || user.role !== "admin") {
282
+ throw new Error("Forbidden");
283
+ }
284
+ return await selectArray(`easyAnalyticsError-${month}` as any);
285
+ },
286
+ // --- ADMIN_ERRORS_WITH_AUTH END ---
287
+
288
+ // --- ADMIN_USERS START ---
289
+ async getUsers(_params: {}, req: any, res: any) {
290
+ const session = await authServer.checkLogin(req!, res!);
291
+ const user = await select("user", session.userId);
292
+ if (!user || user.role !== "admin") {
293
+ throw new Error("Forbidden");
294
+ }
295
+ const allUsers = await selectArray("user");
296
+ return allUsers.map((u: any) => ({
297
+ _id: u._id,
298
+ email: u.mail,
299
+ name: u.name || u.profile?.name || "",
300
+ role: u.profile?.role || u.role || "user"
301
+ }));
302
+ },
303
+ async updateUserRole({ userId, role }: { userId: string; role: "admin" | "user" }, req: any, res: any) {
304
+ const session = await authServer.checkLogin(req!, res!);
305
+ const user = await select("user", session.userId);
306
+ if (!user || user.role !== "admin") {
307
+ throw new Error("Forbidden");
308
+ }
309
+ const targetUser = await select("user", userId);
310
+ if (!targetUser) {
311
+ throw new Error("User not found");
312
+ }
313
+ targetUser.role = role;
314
+ (targetUser as any).profile = (targetUser as any).profile || { name: (targetUser as any).name || "", role: "user" };
315
+ (targetUser as any).profile.role = role;
316
+ await update("user", userId, targetUser);
317
+ return true;
318
+ },
319
+ // --- ADMIN_USERS END ---
320
+ });
321
+
322
+ // --------------------------------- SEO ---------------------------------------
323
+ app.get("/robots.txt", (_: express.Request, res: express.Response) => {
324
+ res.type("text/plain");
325
+ res.send(`User-agent: *\nAllow: /\n\nSitemap: ${ORIGIN}/sitemap.xml`);
140
326
  });
141
327
 
328
+ app.get("/sitemap.xml", async (_: express.Request, res: express.Response) => {
329
+ const sitemap = [{
330
+ url: ORIGIN + "/",
331
+ lastModified: new Date().toISOString().slice(0, 10),
332
+ changeFrequency: "weekly",
333
+ priority: 1,
334
+ }];
335
+
336
+ res.set("Content-Type", "text/xml");
337
+ res.send(`<?xml version="1.0" encoding="UTF-8" ?>
338
+ <urlset xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9 http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd" xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">${sitemap.map(s => `<url>
339
+ <loc>${s.url}</loc>
340
+ <lastmod>${s.lastModified}</lastmod>
341
+ ${s.changeFrequency ? `<changefreq>${s.changeFrequency}</changefreq>` : ""}
342
+ <priority>${s.priority}</priority>
343
+ </url>`).join("\n")}</urlset>`);
344
+ });
142
345
 
143
346
  // --------------------------------- SSR ---------------------------------------
144
- app.get(/.*/, async (req, res) => {
347
+ // --- SSR START ---
348
+ app.get(/.*/, async (req: express.Request, res: express.Response) => {
145
349
  try {
146
350
  const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
147
351
  const html = await renderToHTML(ORIGIN, req.url, indexHtml, <App />);
@@ -149,11 +353,15 @@ app.get(/.*/, async (req, res) => {
149
353
  res.send(html);
150
354
  } catch (e) {
151
355
  console.error("SSR error:", e);
152
- res.status(500);
153
- res.json({ message: "Internal server error" });
356
+ res.status(500).json({ message: "Internal server error" });
154
357
  }
155
358
  });
156
-
359
+ // --- SSR END ---
360
+ // --- NO_SSR START ---
361
+ app.get(/.*/, async (req: express.Request, res: express.Response) => {
362
+ res.sendFile(INDEX_HTML_PATH);
363
+ });
364
+ // --- NO_SSR END ---
157
365
 
158
366
  // Start listening for incoming requests.
159
367
  app.listen(PORT, () => {
@@ -0,0 +1,120 @@
1
+ import nodemailer from "nodemailer";
2
+ import React from "react";
3
+ import { renderToString } from "react-dom/server";
4
+
5
+ const mailHost = process.env.MAIL_SMTP;
6
+ const mailUser = process.env.MAIL_USER;
7
+ const mailPort = Number(process.env.MAIL_PORT || 587);
8
+ const mailPass = process.env.MAIL_PASSWORD;
9
+
10
+ const transporter = (mailHost && mailUser && mailPass) ? nodemailer.createTransport({
11
+ host: mailHost,
12
+ port: mailPort,
13
+ secure: mailPort === 465,
14
+ auth: {
15
+ user: mailUser,
16
+ pass: mailPass,
17
+ },
18
+ }) : null;
19
+
20
+ async function sendMail(mailTo: string, subject: string, html: string) {
21
+ if (!transporter) {
22
+ console.warn("\n⚠️ SMTP is not configured. Email was not sent.");
23
+ console.log(`To: ${mailTo}`);
24
+ console.log(`Subject: ${subject}`);
25
+ console.log(`Content HTML preview:\n${html}\n`);
26
+ return true; // Simulate success in dev if not configured
27
+ }
28
+
29
+ try {
30
+ const info = await transporter.sendMail({
31
+ from: `"My App" <${mailUser}>`,
32
+ to: mailTo,
33
+ subject,
34
+ html,
35
+ });
36
+
37
+ return info.accepted.includes(mailTo);
38
+ } catch (err) {
39
+ console.error("Error sending email:", err);
40
+ return false;
41
+ }
42
+ }
43
+
44
+ const mailTranslations: Record<string, {
45
+ subject: string;
46
+ title: string;
47
+ instruction: string;
48
+ button: string;
49
+ fallback: string;
50
+ disclaimer: string;
51
+ signoff: string;
52
+ }> = {
53
+ cs: {
54
+ subject: "Zapomenuté heslo",
55
+ title: "Zapomenuté heslo",
56
+ instruction: "Pro nastavení nového hesla klikněte na následující tlačítko:",
57
+ button: "Změnit heslo",
58
+ fallback: "Pokud tlačítko nefunguje, zkopírujte tento odkaz do prohlížeče:",
59
+ disclaimer: "Pokud jste o změnu hesla nežádali, tento e-mail prosím ignorujte.",
60
+ signoff: "S pozdravem,",
61
+ },
62
+ en: {
63
+ subject: "Forgotten Password",
64
+ title: "Forgotten Password",
65
+ instruction: "To set your new password, click the following button:",
66
+ button: "Change password",
67
+ fallback: "If the button does not work, try copying this link into your browser:",
68
+ disclaimer: "If you did not request a password change, please ignore this email.",
69
+ signoff: "Kind regards,",
70
+ },
71
+ es: {
72
+ subject: "Contraseña olvidada",
73
+ title: "Contraseña olvidada",
74
+ instruction: "Para configurar tu nueva contraseña, haz clic en el siguiente botón:",
75
+ button: "Cambiar contraseña",
76
+ fallback: "Si el botón no funciona, prueba copiando este enlace en tu navegador:",
77
+ disclaimer: "Si no solicitaste un cambio de contraseña, ignora este correo electrónico.",
78
+ signoff: "Saludos cordiales,",
79
+ }
80
+ };
81
+
82
+ export async function sendMailForgottenPassword(lang: string, mailTo: string, token: string, origin: string) {
83
+ const link = `${origin}/reset-password?token=${encodeURIComponent(token)}`;
84
+ const t = mailTranslations[lang] || mailTranslations.en;
85
+
86
+ const isMailSent = await sendMail(mailTo, t.subject, renderToString(
87
+ <div style={{ fontFamily: "sans-serif", padding: 20, color: "#333" }}>
88
+ <div style={{ backgroundColor: "#333", padding: "20px", color: "#fff", fontSize: "24px", fontWeight: "bold", textAlign: "center", borderRadius: "8px 8px 0 0" }}>
89
+ My App
90
+ </div>
91
+ <div style={{ padding: "20px", border: "1px solid #ddd", borderRadius: "0 0 8px 8px", backgroundColor: "#fafafa" }}>
92
+ <h2>{t.title}</h2>
93
+ <p>{t.instruction}</p>
94
+ <a href={link} target="_blank" rel="noopener noreferrer" style={{
95
+ display: "inline-block",
96
+ margin: "16px 0px",
97
+ padding: "12px 24px",
98
+ borderRadius: "6px",
99
+ backgroundColor: "#6366f1",
100
+ color: "#ffffff",
101
+ textDecoration: "none",
102
+ fontWeight: "bold",
103
+ }}>
104
+ {t.button}
105
+ </a>
106
+ <p style={{ fontSize: "12px", color: "#777" }}>{t.fallback}</p>
107
+ <pre style={{ backgroundColor: "#eee", padding: 10, borderRadius: 4, overflowX: "auto", fontSize: "11px" }}>{link}</pre>
108
+ <p style={{ fontSize: "12px", color: "#999", marginTop: 20 }}>{t.disclaimer}</p>
109
+ <p style={{ borderTop: "1px solid #eee", paddingTop: 15, marginTop: 20 }}>
110
+ {t.signoff}<br />
111
+ <strong>My App Team</strong>
112
+ </p>
113
+ </div>
114
+ </div>
115
+ ));
116
+
117
+ if (!isMailSent) {
118
+ throw new Error("Failed to send forgotten password email.");
119
+ }
120
+ }
@@ -1,15 +1,41 @@
1
- import { init } from "easy-analytics/client";
1
+ import React from "react";
2
2
  import { RouterProvider } from "easy-page-router";
3
3
 
4
4
  import Router from "./Router";
5
5
 
6
+ // --- ANALYTICS START ---
7
+ import { init } from "easy-analytics/client";
8
+
6
9
  // Initialize analytics. It sends data to our backend endpoint.
7
- init(process.env.NODE_ENV === "development"
8
- ? "http://localhost:1111/api/easy-analytics"
9
- : "/api/easy-analytics");
10
+ // In development, Vite proxies this relative path to the Express server.
11
+ init("/api/easy-analytics");
12
+ // --- ANALYTICS END ---
13
+
14
+ // --- AUTH START ---
15
+ import { UserProvider } from "easy-user-auth/react";
16
+ import { UserProfile } from "../types";
17
+ // --- AUTH END ---
10
18
 
11
19
  export default function App() {
12
- return <RouterProvider>
13
- <Router />
14
- </RouterProvider>;
20
+ // --- AUTH START ---
21
+ const defaultUser: UserProfile = {
22
+ name: "",
23
+ role: "user",
24
+ };
25
+
26
+ return (
27
+ <UserProvider<UserProfile> defaultUser={defaultUser}>
28
+ <RouterProvider>
29
+ <Router />
30
+ </RouterProvider>
31
+ </UserProvider>
32
+ );
33
+ // --- AUTH END ---
34
+ // --- NO_AUTH START ---
35
+ return (
36
+ <RouterProvider>
37
+ <Router />
38
+ </RouterProvider>
39
+ );
40
+ // --- NO_AUTH END ---
15
41
  }
@@ -1,16 +1,21 @@
1
+ import React from "react";
1
2
  import { Router } from "easy-page-router/react";
2
3
 
3
4
  import Index from "./pages/Index";
4
5
  import NotFound from "./pages/NotFound";
5
- // --- ADMIN START ---
6
6
 
7
+ // --- ADMIN START ---
7
8
  import { lazy, Suspense } from "react";
8
9
  const Admin = lazy(() => import("./pages/admin/Admin"));
9
10
  // --- ADMIN END ---
10
11
 
12
+ // --- AUTH START ---
13
+ import ResetPassword from "./pages/ResetPassword";
14
+ // --- AUTH END ---
15
+
11
16
  export default function AppRouter() {
12
17
  return <Router
13
- renderPage={({ path }) => {
18
+ renderPage={({ path }: { path: string[] }) => {
14
19
  // The 'path' variable is an array of strings representing the URL segments.
15
20
  // For example, "/users/123" would be ["users", "123"].
16
21
 
@@ -19,12 +24,16 @@ export default function AppRouter() {
19
24
 
20
25
  // --- ADMIN START ---
21
26
  if (path[0] === 'admin') return (
22
- <Suspense fallback={<div style={{ padding: 40, textAlign: 'center' }}>Loading admin...</div>}>
23
- <Admin />
27
+ <Suspense fallback={<div className="loading-fallback">Loading admin...</div>}>
28
+ <Admin path={path} />
24
29
  </Suspense>
25
30
  );
26
31
  // --- ADMIN END ---
27
32
 
33
+ // --- AUTH START ---
34
+ if (path[0] === 'reset-password') return <ResetPassword />;
35
+ // --- AUTH END ---
36
+
28
37
  // You can add more routes here, for example:
29
38
  // if (path[0] === 'about') return <AboutPage />;
30
39
  // if (path[0] === 'users' && path[1]) return <UserPage id={path[1]} />;