easy-starter 2.0.0 → 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.
@@ -25,6 +25,10 @@ import { UserProfile } from "../types";
25
25
  import puppeteer from "puppeteer";
26
26
  // --- OGIMAGE END ---
27
27
 
28
+ // --- RESIZER START ---
29
+ import { expressImageResizer } from "easy-image-resizer";
30
+ // --- RESIZER END ---
31
+
28
32
  import App from "../src/App";
29
33
  import { API, Database } from "../types";
30
34
 
@@ -69,21 +73,21 @@ const authServer = new EasyLoginServer<UserProfile>({
69
73
  jwtSecret: process.env.JWT_SECRET || "fallback-secret-key-change-me",
70
74
  secureCookies: process.env.NODE_ENV === "production",
71
75
  db: {
72
- insertUser: async (user) => insert("user", user),
73
- getUserById: async (id) => select("user", id),
74
- getUserByMail: async (mail) => {
76
+ insertUser: async (user: any) => insert("user", user),
77
+ getUserById: async (id: string) => select("user", id),
78
+ getUserByMail: async (mail: string) => {
75
79
  const users = await selectArray("user");
76
- return users.find(u => u.mail.toLowerCase() === mail.toLowerCase()) || null;
80
+ return users.find((u: any) => u.mail.toLowerCase() === mail.toLowerCase()) || null;
77
81
  },
78
- getUserByRecoveryToken: async (token) => {
82
+ getUserByRecoveryToken: async (token: string) => {
79
83
  const users = await selectArray("user");
80
- return users.find(u => u.passwordRecovery?.token === token) || null;
84
+ return users.find((u: any) => u.passwordRecovery?.token === token) || null;
81
85
  },
82
- updateUser: async (id, user) => {
86
+ updateUser: async (id: string, user: any) => {
83
87
  await update("user", id, user);
84
88
  },
85
89
  },
86
- mailSender: async (lang, mailTo, token) => {
90
+ mailSender: async (lang: string, mailTo: string, token: string) => {
87
91
  await sendMailForgottenPassword(lang, mailTo, token, ORIGIN);
88
92
  },
89
93
  });
@@ -92,6 +96,14 @@ const authServer = new EasyLoginServer<UserProfile>({
92
96
  authServer.registerExpressRoutes(app);
93
97
  // --- AUTH END ---
94
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
+
95
107
  // Serve static files generated by the bundler (JS, CSS)
96
108
  app.use(express.static("dist"));
97
109
  // Serve any public assets (like images, fonts)
@@ -135,7 +147,7 @@ app.get("/og-image.png", async (req, res) => {
135
147
  await page.setJavaScriptEnabled(true);
136
148
  // --- NO_SSR END ---
137
149
  await page.setViewport({ width: 600, height: 500 });
138
-
150
+
139
151
  // Render target page
140
152
  await page.goto(`${LOCALHOST}${path}`, { waitUntil: 'networkidle0' });
141
153
  const base64 = await page.screenshot({ type: "png", encoding: "base64" });
@@ -213,16 +225,16 @@ setAPIBackend<API>(app, {
213
225
  return item;
214
226
  },
215
227
  async addItem({ name, value }) {
216
- const id = await insert("item", { name, value });
228
+ const id = await insert("item", { name, value } as any);
217
229
  return id;
218
230
  },
219
231
  async removeItem({ id }) {
220
232
  await remove("item", id);
221
233
  return true;
222
234
  },
223
-
224
- // --- ADMIN_NO_AUTH START ---
225
- async getEasyAnalyticsData({ password, month }) {
235
+
236
+ /* --- ADMIN_NO_AUTH START --- */
237
+ async getEasyAnalyticsData({ password, month }: { password?: string; month: string }) {
226
238
  const validPassword = process.env.ADMIN_PASSWORD;
227
239
  if (!validPassword || password !== validPassword) {
228
240
  throw new Error("Invalid password or server error");
@@ -234,10 +246,10 @@ setAPIBackend<API>(app, {
234
246
  return [];
235
247
  // --- NO_ANALYTICS END ---
236
248
  },
237
- // --- ADMIN_NO_AUTH END ---
249
+ /* --- ADMIN_NO_AUTH END --- */
238
250
 
239
251
  // --- ADMIN_WITH_AUTH START ---
240
- async getEasyAnalyticsData({ month }, req, res) {
252
+ async getEasyAnalyticsData({ month }: { month: string }, req?: any, res?: any) {
241
253
  const session = await authServer.checkLogin(req!, res!);
242
254
  const user = await select("user", session.userId);
243
255
  if (!user || user.role !== "admin") {
@@ -252,43 +264,43 @@ setAPIBackend<API>(app, {
252
264
  },
253
265
  // --- ADMIN_WITH_AUTH END ---
254
266
 
255
- // --- ADMIN_ERRORS_NO_AUTH START ---
256
- async getEasyAnalyticsErrors({ password, month }) {
267
+ /* --- ADMIN_ERRORS_NO_AUTH START --- */
268
+ async getEasyAnalyticsErrors({ password, month }: { password?: string; month: string }) {
257
269
  const validPassword = process.env.ADMIN_PASSWORD;
258
270
  if (!validPassword || password !== validPassword) {
259
271
  throw new Error("Invalid password or server error");
260
272
  }
261
- return await selectArray(`easyAnalyticsError-${month}`);
273
+ return await selectArray(`easyAnalyticsError-${month}` as any);
262
274
  },
263
- // --- ADMIN_ERRORS_NO_AUTH END ---
275
+ /* --- ADMIN_ERRORS_NO_AUTH END --- */
264
276
 
265
277
  // --- ADMIN_ERRORS_WITH_AUTH START ---
266
- async getEasyAnalyticsErrors({ month }, req, res) {
278
+ async getEasyAnalyticsErrors({ month }: { month: string }, req?: any, res?: any) {
267
279
  const session = await authServer.checkLogin(req!, res!);
268
280
  const user = await select("user", session.userId);
269
281
  if (!user || user.role !== "admin") {
270
282
  throw new Error("Forbidden");
271
283
  }
272
- return await selectArray(`easyAnalyticsError-${month}`);
284
+ return await selectArray(`easyAnalyticsError-${month}` as any);
273
285
  },
274
286
  // --- ADMIN_ERRORS_WITH_AUTH END ---
275
287
 
276
288
  // --- ADMIN_USERS START ---
277
- async getUsers(params, req, res) {
289
+ async getUsers(_params: {}, req: any, res: any) {
278
290
  const session = await authServer.checkLogin(req!, res!);
279
291
  const user = await select("user", session.userId);
280
292
  if (!user || user.role !== "admin") {
281
293
  throw new Error("Forbidden");
282
294
  }
283
295
  const allUsers = await selectArray("user");
284
- return allUsers.map(u => ({
296
+ return allUsers.map((u: any) => ({
285
297
  _id: u._id,
286
298
  email: u.mail,
287
299
  name: u.name || u.profile?.name || "",
288
300
  role: u.profile?.role || u.role || "user"
289
301
  }));
290
302
  },
291
- async updateUserRole({ userId, role }, req, res) {
303
+ async updateUserRole({ userId, role }: { userId: string; role: "admin" | "user" }, req: any, res: any) {
292
304
  const session = await authServer.checkLogin(req!, res!);
293
305
  const user = await select("user", session.userId);
294
306
  if (!user || user.role !== "admin") {
@@ -299,8 +311,8 @@ setAPIBackend<API>(app, {
299
311
  throw new Error("User not found");
300
312
  }
301
313
  targetUser.role = role;
302
- targetUser.profile = targetUser.profile || { name: targetUser.name || "", role: "user" };
303
- targetUser.profile.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;
304
316
  await update("user", userId, targetUser);
305
317
  return true;
306
318
  },
@@ -308,12 +320,12 @@ setAPIBackend<API>(app, {
308
320
  });
309
321
 
310
322
  // --------------------------------- SEO ---------------------------------------
311
- app.get("/robots.txt", (_, res) => {
323
+ app.get("/robots.txt", (_: express.Request, res: express.Response) => {
312
324
  res.type("text/plain");
313
325
  res.send(`User-agent: *\nAllow: /\n\nSitemap: ${ORIGIN}/sitemap.xml`);
314
326
  });
315
327
 
316
- app.get("/sitemap.xml", async (_, res) => {
328
+ app.get("/sitemap.xml", async (_: express.Request, res: express.Response) => {
317
329
  const sitemap = [{
318
330
  url: ORIGIN + "/",
319
331
  lastModified: new Date().toISOString().slice(0, 10),
@@ -333,7 +345,7 @@ app.get("/sitemap.xml", async (_, res) => {
333
345
 
334
346
  // --------------------------------- SSR ---------------------------------------
335
347
  // --- SSR START ---
336
- app.get(/.*/, async (req, res) => {
348
+ app.get(/.*/, async (req: express.Request, res: express.Response) => {
337
349
  try {
338
350
  const indexHtml = await readFile(INDEX_HTML_PATH, "utf-8");
339
351
  const html = await renderToHTML(ORIGIN, req.url, indexHtml, <App />);
@@ -346,7 +358,7 @@ app.get(/.*/, async (req, res) => {
346
358
  });
347
359
  // --- SSR END ---
348
360
  // --- NO_SSR START ---
349
- app.get(/.*/, async (req, res) => {
361
+ app.get(/.*/, async (req: express.Request, res: express.Response) => {
350
362
  res.sendFile(INDEX_HTML_PATH);
351
363
  });
352
364
  // --- NO_SSR END ---
@@ -15,7 +15,7 @@ import ResetPassword from "./pages/ResetPassword";
15
15
 
16
16
  export default function AppRouter() {
17
17
  return <Router
18
- renderPage={({ path }) => {
18
+ renderPage={({ path }: { path: string[] }) => {
19
19
  // The 'path' variable is an array of strings representing the URL segments.
20
20
  // For example, "/users/123" would be ["users", "123"].
21
21
 
@@ -24,7 +24,7 @@ export default function AppRouter() {
24
24
 
25
25
  // --- ADMIN START ---
26
26
  if (path[0] === 'admin') return (
27
- <Suspense fallback={<div style={{ padding: 40, textAlign: 'center' }}>Loading admin...</div>}>
27
+ <Suspense fallback={<div className="loading-fallback">Loading admin...</div>}>
28
28
  <Admin path={path} />
29
29
  </Suspense>
30
30
  );
@@ -0,0 +1,127 @@
1
+ import React from "react";
2
+
3
+ export type ImgProps = React.ImgHTMLAttributes<HTMLImageElement> & {
4
+ /** Target width in pixels */
5
+ w?: number | string;
6
+ /** Target height in pixels */
7
+ h?: number | string;
8
+ /** Max width in pixels */
9
+ mw?: number | string;
10
+ /** Max height in pixels */
11
+ mh?: number | string;
12
+ /** Max width in pixels (alias) */
13
+ maxWidth?: number | string;
14
+ /** Max height in pixels (alias) */
15
+ maxHeight?: number | string;
16
+ /** Output image format (default: "webp") */
17
+ format?: "webp" | "jpeg" | "png";
18
+ /** Output image format (alias) */
19
+ type?: "webp" | "jpeg" | "png";
20
+ /** Quality from 1 to 100 */
21
+ quality?: number;
22
+ /** Quality from 1 to 100 (alias) */
23
+ q?: number;
24
+ /** Smooth resize processing */
25
+ smooth?: boolean;
26
+ /** Smooth resize processing (alias) */
27
+ s?: boolean;
28
+ };
29
+
30
+ export function imgSrc(
31
+ src: string,
32
+ options: {
33
+ w?: number | string;
34
+ h?: number | string;
35
+ mw?: number | string;
36
+ mh?: number | string;
37
+ format?: "webp" | "jpeg" | "png";
38
+ quality?: number;
39
+ smooth?: boolean;
40
+ }
41
+ ): string | undefined {
42
+ if (!src) return src;
43
+
44
+ const targetFormat = options.format || "webp";
45
+
46
+ try {
47
+ const [base, queryString] = src.split("?");
48
+ const params = new URLSearchParams(queryString || "");
49
+
50
+ if (options.w !== undefined && !params.has("w") && !params.has("width")) {
51
+ params.set("w", String(options.w));
52
+ }
53
+ if (options.h !== undefined && !params.has("h") && !params.has("height")) {
54
+ params.set("h", String(options.h));
55
+ }
56
+ if (options.mw !== undefined && !params.has("mw") && !params.has("maxWidth")) {
57
+ params.set("mw", String(options.mw));
58
+ }
59
+ if (options.mh !== undefined && !params.has("mh") && !params.has("maxHeight")) {
60
+ params.set("mh", String(options.mh));
61
+ }
62
+ if (options.quality !== undefined && !params.has("q") && !params.has("quality")) {
63
+ params.set("q", String(options.quality));
64
+ }
65
+ if (options.smooth !== undefined && !params.has("s") && !params.has("smooth")) {
66
+ params.set("s", String(options.smooth));
67
+ }
68
+ if (targetFormat && !params.has("format") && !params.has("type") && !params.has("ext")) {
69
+ params.set("format", targetFormat);
70
+ }
71
+
72
+ const finalQuery = params.toString();
73
+ return finalQuery ? `${base}?${finalQuery}` : base;
74
+ } catch {
75
+ return src;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Smart image component wrapping HTML <img> with automatic easy-image-resizer URL formatting.
81
+ * Defaults to WebP format with optional width/height/quality scaling options.
82
+ */
83
+ export function Img({
84
+ src,
85
+ width,
86
+ height,
87
+ w,
88
+ h,
89
+ mw,
90
+ mh,
91
+ maxWidth,
92
+ maxHeight,
93
+ format,
94
+ type,
95
+ quality,
96
+ q,
97
+ smooth,
98
+ s,
99
+ ...restProps
100
+ }: ImgProps) {
101
+ const finalW = w ?? width;
102
+ const finalH = h ?? height;
103
+ const finalMw = mw ?? maxWidth;
104
+ const finalMh = mh ?? maxHeight;
105
+ const finalFormat = format ?? type ?? "webp";
106
+ const finalQuality = q ?? quality;
107
+ const finalSmooth = s ?? smooth;
108
+
109
+ const finalSrc = imgSrc(src || "", {
110
+ w: finalW,
111
+ h: finalH,
112
+ mw: finalMw,
113
+ mh: finalMh,
114
+ format: finalFormat,
115
+ quality: finalQuality,
116
+ smooth: finalSmooth,
117
+ });
118
+
119
+ return <img
120
+ src={finalSrc}
121
+ width={finalW}
122
+ height={finalH}
123
+ {...restProps}
124
+ />;
125
+ };
126
+
127
+ export default Img;
@@ -12,18 +12,9 @@ import { UserProfile } from "../../types";
12
12
  // --- AUTH END ---
13
13
 
14
14
  export default function Index() {
15
- // --- OGIMAGE START ---
16
- const ogImage = typeof window !== "undefined"
17
- ? `${window.location.origin}/og-image.png?path=${encodeURIComponent(window.location.pathname)}`
18
- : undefined;
19
- // --- OGIMAGE END ---
20
-
21
15
  useHead({
22
16
  title: "Welcome to easy-starter",
23
17
  description: "The ultimate foundation for your next project."
24
- // --- OGIMAGE START ---
25
- , image: ogImage
26
- // --- OGIMAGE END ---
27
18
  });
28
19
 
29
20
  // --- AUTH START ---
@@ -69,9 +60,9 @@ export default function Index() {
69
60
  return (
70
61
  <main>
71
62
  {/* --- AUTH START --- */}
72
- <div style={{ display: "flex", justifyContent: "flex-end", padding: "10px 0", borderBottom: "1px solid var(--border)", marginBottom: 20 }}>
63
+ <div className="auth-bar">
73
64
  {isLoggedIn ? (
74
- <div style={{ display: "flex", alignItems: "center", gap: 15 }}>
65
+ <div className="auth-user-info">
75
66
  <span>Logged in as: <strong>{user.name} ({user.role})</strong></span>
76
67
  <button onClick={() => loginUser(null)}>Log Out</button>
77
68
  </div>
@@ -85,9 +76,9 @@ export default function Index() {
85
76
  <figure></figure>
86
77
  <h1>Welcome to easy-starter!</h1>
87
78
  <p>The ultimate foundation for your next project.</p>
88
-
79
+
89
80
  {/* --- ADMIN START --- */}
90
- <div style={{ marginTop: 20 }}>
81
+ <div className="admin-link-wrapper">
91
82
  <Link href="/admin">
92
83
  → Go to Admin Dashboard
93
84
  </Link>
@@ -6,7 +6,7 @@ export default function NotFound() {
6
6
  useHead({ title: "404 Not Found", description: "Page you are looking for does not exist." });
7
7
 
8
8
  return (
9
- <div style={{ padding: "40px", textAlign: "center", fontFamily: "sans-serif" }}>
9
+ <div className="not-found">
10
10
  <h1>404</h1>
11
11
  <p>Oops, page not found.</p>
12
12
  <Link href="/">Go back home</Link>
@@ -58,15 +58,15 @@ export default function ResetPasswordPage() {
58
58
  };
59
59
 
60
60
  return (
61
- <main style={{ maxWidth: 450, margin: "60px auto" }}>
62
- <h2 className="easy-user-auth-title" style={{ marginBottom: 20 }}>🔑 {dict.userForgottenPasswordChange}</h2>
61
+ <main className="reset-password-main">
62
+ <h2 className="easy-user-auth-title">🔑 {dict.userForgottenPasswordChange}</h2>
63
63
 
64
64
  {error && !token && (
65
- <div className="easy-user-auth-alert" style={{ marginBottom: 20 }}>{error}</div>
65
+ <div className="easy-user-auth-alert">{error}</div>
66
66
  )}
67
67
 
68
68
  {success ? (
69
- <div className="easy-user-auth-alert" style={{ background: "rgba(16, 185, 129, 0.1)", border: "1px solid rgba(16, 185, 129, 0.2)", color: "#34d399", marginBottom: 20 }}>
69
+ <div className="easy-user-auth-alert alert-success">
70
70
  {dict.userForgottenPasswordSuccessChange}
71
71
  </div>
72
72
  ) : (
@@ -97,14 +97,13 @@ export default function ResetPasswordPage() {
97
97
  </div>
98
98
 
99
99
  {error && <div className="easy-user-auth-alert">{error}</div>}
100
- {validationError && <p className="easy-user-auth-info" style={{ color: "#ef4444" }}>{validationError}</p>}
100
+ {validationError && <p className="easy-user-auth-info text-error">{validationError}</p>}
101
101
 
102
- <div style={{ display: "flex", gap: 15, marginTop: 10 }}>
102
+ <div className="btn-group">
103
103
  <button
104
104
  type="submit"
105
105
  className="easy-user-auth-submit-btn"
106
106
  disabled={!!validationError || !password || !passwordAgain || loading}
107
- style={{ flex: 1 }}
108
107
  >
109
108
  {loading ? "Updating..." : dict.change}
110
109
  </button>
@@ -113,7 +112,6 @@ export default function ResetPasswordPage() {
113
112
  className="easy-user-auth-back-btn"
114
113
  onClick={() => push("/")}
115
114
  disabled={loading}
116
- style={{ flex: 1 }}
117
115
  >
118
116
  Cancel
119
117
  </button>