@flun/html-template 4.4.3 → 5.0.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.
@@ -29,9 +29,7 @@ if (!env.MAIL_HOST || !env.MAIL_USER || !env.MAIL_PWD)
29
29
 
30
30
  // 邮件发送配置
31
31
  const transporter = createTransport({
32
- host: env.MAIL_HOST,
33
- port: env.MAIL_PORT,
34
- secure: true,
32
+ host: env.MAIL_HOST, port: env.MAIL_PORT, secure: true,
35
33
  auth: { user: env.MAIL_USER, pass: env.MAIL_PWD }
36
34
  }),
37
35
  // ========== 辅助函数 ==========
@@ -51,10 +49,10 @@ const transporter = createTransport({
51
49
  emailVerified, emailVerificationToken: null,
52
50
  passwordResetToken: null, passwordResetExpires: null,
53
51
  twoFactorSecret: null, twoFactorEnabled: false,
54
- backupCodes: [],
55
52
  webauthnCredentials: [], webauthnEnabled: false,
56
53
  createdAt: now, updatedAt: now, passwordChangedAt: now,
57
- pendingEmail: null, pendingEmailToken: null, pendingEmailExpires: null
54
+ pendingEmail: null, pendingEmailToken: null, pendingEmailExpires: null,
55
+ backupCodes: [], codeFailures: 0, codeLockUntil: null
58
56
  };
59
57
  },
60
58
  initAdminUser = () => {
@@ -66,7 +64,7 @@ const transporter = createTransport({
66
64
  }
67
65
  },
68
66
  sendEmail = async (to, subject, html) => {
69
- if (transporter) await transporter.sendMail({ from: `"Your App" <${env.MAIL_USER}>`, to, subject, html });
67
+ if (transporter) await transporter.sendMail({ from: `"我的网站" <${env.MAIL_USER}>`, to, subject, html });
70
68
  else console.log(`\n--- 模拟邮件 ---\n收件人: ${to}\n主题: ${subject}\n内容:\n${html}\n---`);
71
69
  },
72
70
  validateEmail = email => mailRegex.test(email), generateToken = () => randomBytes(32).toString('hex'),
@@ -132,13 +130,17 @@ const transporter = createTransport({
132
130
  subject = '添加验证硬件通知';
133
131
  actionDescription = '添加了一台新的硬件验证设备';
134
132
  break;
133
+ case 'backup_code_lock': // 新增:备份码锁定
134
+ subject = '安全警报:账户临时锁定';
135
+ actionDescription = '连续多次输入错误的备用码,账户已被临时锁定24小时';
136
+ break;
135
137
  default: return;
136
138
  }
137
139
 
138
140
  const html = `
139
- <p>您好!</p>
140
- <p>您的账户(${user.username})于 <strong>${now}</strong>在 <strong>${clientIp}</strong> 成功${actionDescription};</p>
141
- <p>如果是您本人操作,请忽略此邮件,否则请立即处理;</p>
141
+ <p>主人好!</p>
142
+ <p>网站账户(${user.username})于 <strong>${now}</strong>在 <strong>${clientIp}</strong> ${actionDescription};</p>
143
+ <p>如果是您的操作,请忽略此邮件,否则请前往处理;</p>
142
144
  <p style="margin-left:65%;">此致<br/>&emsp;&nbsp;安全中心</p>
143
145
  `;
144
146
  await sendEmail(user.email, subject, html);
@@ -180,16 +182,14 @@ export const accountRouter = app => {
180
182
  set(sid, session, cb) {
181
183
  const file = path.join(this.sessionsDir, `${sid}.json`);
182
184
  try {
183
- fs.writeFileSync(file, JSON.stringify(session));
184
- cb(null);
185
+ fs.writeFileSync(file, JSON.stringify(session)), cb(null);
185
186
  } catch (e) { cb(e); }
186
187
  }
187
188
 
188
189
  destroy(sid, cb) {
189
190
  const file = path.join(this.sessionsDir, `${sid}.json`);
190
191
  try {
191
- if (fs.existsSync(file)) fs.unlinkSync(file);
192
- cb(null);
192
+ if (fs.existsSync(file)) fs.unlinkSync(file), cb(null);
193
193
  } catch (e) { cb(e); }
194
194
  }
195
195
 
@@ -225,11 +225,9 @@ export const accountRouter = app => {
225
225
  app.use(session({
226
226
  secret: env.SESSION_SECRET || 'dev-secret-change-in-production',
227
227
  store: sessionStore,
228
- resave: false,
229
- saveUninitialized: false,
228
+ resave: false, saveUninitialized: false,
230
229
  cookie: { secure: false, httpOnly: true, sameSite: 'lax', maxAge: 30 * 24 * oneHour }
231
230
  }));
232
-
233
231
  app.use(express.json(), express.urlencoded({ extended: true })), initAdminUser();
234
232
 
235
233
  // 2. 全局登录保护中间件
@@ -260,9 +258,7 @@ export const accountRouter = app => {
260
258
  const sessionLoginTime = req.session.loginTime || 0;
261
259
  if (user.passwordChangedAt > sessionLoginTime) {
262
260
  req.session.destroy(() => {
263
- if (req.path.startsWith('/api/')) {
264
- return res.status(401).json({ message: '密码已修改,请重新登录' });
265
- }
261
+ if (req.path.startsWith('/api/')) return res.status(401).json({ message: '密码已修改,请重新登录' });
266
262
  return res.redirect('/login');
267
263
  });
268
264
  return;
@@ -281,7 +277,7 @@ export const accountRouter = app => {
281
277
  }, oneHour);
282
278
 
283
279
  // 定义安全限制和所有页面(公共页面 + 受保护页面)
284
- const authLimiter = rateLimit({ windowMs: fifteenMin, max: 50, message: { message: '尝试次数过多,请稍后再试' } }),
280
+ const authLimiter = rateLimit({ windowMs: fifteenMin, max: 15, message: { message: '尝试次数过多,请稍后再试' } }),
285
281
  allPages = [...publicPage, '/profile'];
286
282
  allPages.forEach(page => {
287
283
  app.get(page, (req, res) => {
@@ -306,8 +302,7 @@ export const accountRouter = app => {
306
302
  // ========== 公开认证API ==========
307
303
  app.post('/api/register', authLimiter, async (req, res) => {
308
304
  const { username, email, password } = req.body;
309
- if (!hasAllFields(req.body, ['username', 'email', 'password']))
310
- return res.status(400).json({ message: '所有字段必填' });
305
+ if (!hasAllFields(req.body, ['username', 'email', 'password'])) return res.status(400).json({ message: '所有字段必填' });
311
306
  if (!validateEmail(email)) return res.status(400).json({ message: '邮箱格式不正确' });
312
307
  if (!validatePasswordLength(password)) return res.status(400).json({ message: '密码至少6位' });
313
308
 
@@ -354,12 +349,15 @@ export const accountRouter = app => {
354
349
  const valid = await verifyPassword(password, user.password);
355
350
  if (!valid) return res.status(401).json({ message: '用户名/邮箱或密码错误' });
356
351
  if (!user.emailVerified) return res.status(403).json({ message: '请先验证邮箱', needsVerification: true });
357
- if (user.webauthnEnabled && user.webauthnCredentials.length > 0)
358
- return req.session.tempUserId = user.id, res.json({ requireWebAuthn: true });
359
- if (user.twoFactorEnabled) return req.session.tempUserId = user.id, res.json({ require2FA: true });
360
352
 
353
+ req.session.tempUserId = user.id; // 保存临时用户ID,供后续多因素验证使用
354
+ if (user.webauthnEnabled && user.webauthnCredentials.length > 0) return res.json({ requireWebAuthn: true });
355
+ if (user.twoFactorEnabled) return res.json({ require2FA: true });
356
+
357
+ // 没有多因素认证,直接登录
361
358
  req.session.userId = user.id, req.session.username = user.username, req.session.loginTime = Date.now();
362
- recentPasswordResets.delete(user.email), res.json({ success: true, message: '登录成功' });
359
+ delete req.session.tempUserId, recentPasswordResets.delete(user.email);
360
+ res.json({ success: true, message: '登录成功' });
363
361
  });
364
362
 
365
363
  app.post('/api/verify-2fa', authLimiter, async (req, res) => {
@@ -370,25 +368,59 @@ export const accountRouter = app => {
370
368
  if (!tempUserId) return res.status(401).json({ message: '请先完成第一步登录' });
371
369
 
372
370
  const users = readUsers(), user = users.find(u => u.id === tempUserId);
373
- if (!user?.twoFactorEnabled) return res.status(400).json({ message: '用户未启用2FA' });
371
+ if (!user?.twoFactorEnabled && !user?.webauthnEnabled)
372
+ return res.status(400).json({ message: '用户未启用多因素认证' });
373
+
374
+ // 过期锁定自动清除
375
+ if (user.codeLockUntil && user.codeLockUntil <= Date.now())
376
+ user.codeLockUntil = null, user.codeFailures = 0, touchAndSaveUser(users, user);
377
+ else if (user.codeLockUntil && user.codeLockUntil > Date.now())
378
+ return res.status(401).json({ message: '账户已被临时锁定,请24小时后重试', locked: true });
379
+
380
+ const { twoFactorSecret, backupCodes } = user;
381
+ let verified = false, backupValid = false;
382
+
383
+ // TOTP 验证(6位)
384
+ if (token.length === 6) {
385
+ try {
386
+ verified = await verifyTotp(twoFactorSecret, token);
387
+ } catch (err) {
388
+ console.error('TOTP 验证异常:', err.message);
389
+ }
390
+ }
374
391
 
375
- const { twoFactorSecret, backupCodes } = user, verified = await verifyTotp(twoFactorSecret, token);
376
- let backupValid = false;
377
- if (!verified && backupCodes?.length)
392
+ // 备份码验证(长度:10位)
393
+ if (!verified && backupCodes?.length) {
378
394
  for (let i = 0; i < backupCodes.length; i++) {
379
395
  const match = await verifyPassword(token, backupCodes[i]);
380
396
  if (match) {
381
- backupValid = true, backupCodes.splice(i, 1), touchAndSaveUser(users, user);
397
+ backupValid = true, backupCodes.splice(i, 1);
382
398
  break;
383
399
  }
384
400
  }
401
+ }
385
402
 
403
+ // 输入错误失败计数与锁定
404
+ if (!verified && !backupValid) {
405
+ user.codeFailures = (user.codeFailures || 0) + 1;
406
+ if (user.codeFailures >= 9) {
407
+ user.codeLockUntil = Date.now() + 24 * oneHour;
408
+ await sendSecurityAlertEmail(req, user, 'backup_code_lock'), touchAndSaveUser(users, user);
409
+ return res.status(401).json({ message: '输入错误次数过多,账户已被锁定24小时,请隔天重试或联系管理员', locked: true });
410
+ } else {
411
+ const remaining = 9 - user.codeFailures;
412
+ touchAndSaveUser(users, user);
413
+ return res.status(401).json({ message: `输入错误,剩余尝试次数:${remaining}`, remainingAttempts: remaining });
414
+ }
415
+ }
416
+
417
+ // 验证成功,重置所有限制
386
418
  if (verified || backupValid) {
387
- delete req.session.tempUserId, req.session.userId = user.id, req.session.username = user.username;
388
- req.session.loginTime = Date.now(), recentPasswordResets.delete(user.email);
389
- res.json({ success: true, message: '2FA验证成功' });
419
+ user.codeFailures = 0, user.codeLockUntil = null, delete req.session.tempUserId;
420
+ req.session.userId = user.id, req.session.username = user.username, req.session.loginTime = Date.now();
421
+ recentPasswordResets.delete(user.email), touchAndSaveUser(users, user);
422
+ return res.json({ success: true, message: '二次验证成功' });
390
423
  }
391
- else res.status(401).json({ message: '验证码无效' });
392
424
  });
393
425
 
394
426
  app.post('/api/logout', (req, res) => {
@@ -452,10 +484,10 @@ export const accountRouter = app => {
452
484
  const result = getCurrentUser(req);
453
485
  if (!result) return res.status(404).json({ message: '用户不存在' });
454
486
  const { id, username, email, emailVerified, twoFactorEnabled, createdAt, pendingEmail, webauthnEnabled,
455
- webauthnCredentials } = result.user;
487
+ webauthnCredentials, backupCodes } = result.user;
456
488
  res.json({
457
489
  id, username, email, emailVerified, twoFactorEnabled, createdAt, pendingEmail, webauthnEnabled,
458
- webauthnCredentials
490
+ webauthnCredentials, hasBackupCodes: !!(backupCodes?.length > 0)
459
491
  });
460
492
  });
461
493
 
@@ -523,7 +555,7 @@ export const accountRouter = app => {
523
555
  if (!result) return res.status(404).json({ message: '用户不存在' });
524
556
 
525
557
  const { user, users } = result, secret = generateSecret({ length: 32 }), otpauth_url = generateURI({
526
- issuer: 'YourApp', label: user.username, secret,
558
+ issuer: '我的网站', label: user.username, secret,
527
559
  }), qrCodeUrl = await toDataURL(otpauth_url);
528
560
 
529
561
  user.twoFactorSecret = secret, touchAndSaveUser(users, user), res.json({ secret, qrCode: qrCodeUrl });
@@ -539,8 +571,14 @@ export const accountRouter = app => {
539
571
  const { user, users } = result, verified = await verifyTotp(user.twoFactorSecret, token);
540
572
  if (!verified) return res.status(400).json({ message: '验证码错误' });
541
573
 
542
- const { plainCodes, hashedCodes } = generateBackupCodes();
543
- user.backupCodes = hashedCodes, user.twoFactorEnabled = true, touchAndSaveUser(users, user);
574
+ let plainCodes = null;
575
+ // 仅当用户当前没有备份码时,生成一组新的
576
+ if (!user.backupCodes || user.backupCodes.length === 0) {
577
+ const { plainCodes: newPlain, hashedCodes } = generateBackupCodes();
578
+ user.backupCodes = hashedCodes, plainCodes = newPlain;;
579
+ }
580
+
581
+ user.twoFactorEnabled = true, touchAndSaveUser(users, user);
544
582
  res.json({ success: true, backupCodes: plainCodes, message: '2FA 已启用' });
545
583
  });
546
584
 
@@ -549,20 +587,26 @@ export const accountRouter = app => {
549
587
  if (!result) return res.status(404).json({ message: '用户不存在' });
550
588
 
551
589
  const { user, users } = result;
552
- user.twoFactorEnabled = false, user.twoFactorSecret = null, user.backupCodes = [];
553
- touchAndSaveUser(users, user), res.json({ success: true });
590
+ user.twoFactorEnabled = false, user.twoFactorSecret = null, touchAndSaveUser(users, user), res.json({ success: true });
554
591
  });
555
592
 
556
593
  app.post('/api/regenerate-backup-codes', async (req, res) => {
557
594
  const result = getCurrentUser(req);
558
595
  if (!result) return res.status(404).json({ message: '用户不存在' });
559
596
  const { user, users } = result;
560
- if (!user.twoFactorEnabled) return res.status(400).json({ message: '2FA未启用' });
597
+ if (!user.twoFactorEnabled && !user.webauthnEnabled) return res.status(400).json({ message: '未启用任何多因素认证' });
561
598
 
562
599
  const { plainCodes, hashedCodes } = generateBackupCodes();
563
600
  user.backupCodes = hashedCodes, touchAndSaveUser(users, user), res.json({ backupCodes: plainCodes });
564
601
  });
565
602
 
603
+ app.post('/api/delete-backup-codes', async (req, res) => {
604
+ const result = getCurrentUser(req);
605
+ if (!result) return res.status(401).json({ message: '未登录' });
606
+ const { user, users } = result;
607
+ user.backupCodes = [], touchAndSaveUser(users, user), res.json({ success: true, message: '备份码已删除' });
608
+ });
609
+
566
610
  // ========== WebAuthn 硬件验证 API ==========
567
611
  app.post('/api/webauthn/register/begin', authLimiter, async (req, res) => {
568
612
  try {
@@ -570,20 +614,18 @@ export const accountRouter = app => {
570
614
  if (!result) return res.status(401).json({ message: '未登录' });
571
615
  const { id, username, webauthnCredentials } = result.user, rpID = getRpId(req),
572
616
  options = await generateRegistrationOptions({
573
- rpName: 'Your App',
617
+ rpName: '我的网站',
574
618
  rpID, userID: Buffer.from(String(id)),
575
619
  userName: username, userDisplayName: username,
576
620
  attestationType: 'none',
577
621
  excludeCredentials: webauthnCredentials.map(cred => ({
578
- id: cred.id,
579
- type: 'public-key',
580
- transports: cred.transports
622
+ id: cred.id, type: 'public-key', transports: cred.transports
581
623
  })),
582
624
  authenticatorSelection: { userVerification: 'preferred' },
583
625
  });
584
626
  req.session.webauthnRegisterChallenge = options.challenge, res.json(options);
585
627
  } catch (err) {
586
- return res.status(400).json({ message: err.message || '硬件验证初始化失败,请确保使用 HTTPS 或 localhost 访问;' });
628
+ return res.status(400).json({ message: err.message });
587
629
  }
588
630
  });
589
631
 
@@ -615,18 +657,27 @@ export const accountRouter = app => {
615
657
 
616
658
  const clientIp = getClientIp(req), { id, publicKey, counter, transports = [], deviceType, backedUp } = cred,
617
659
  newCredential = {
618
- id,
619
- publicKey: fromBuffer(publicKey),
620
- counter: Number(counter),
660
+ id, publicKey: fromBuffer(publicKey), counter: Number(counter),
621
661
  transports, deviceType, backedUp,
622
662
  deviceName: clientIp, createdAt: Date.now()
623
663
  };
624
664
 
625
665
  result.user.webauthnCredentials.push(newCredential);
626
666
  if (!result.user.webauthnEnabled) result.user.webauthnEnabled = true;
667
+
668
+ // 如果用户还没有备份码且至少启用了一个多因素认证,则自动生成备份码
669
+ let newBackupCodes = null;
670
+ if ((!result.user.backupCodes || result.user.backupCodes.length === 0) && !result.user.twoFactorEnabled) {
671
+ const { plainCodes, hashedCodes } = generateBackupCodes();
672
+ result.user.backupCodes = hashedCodes, newBackupCodes = plainCodes;
673
+ }
674
+
627
675
  touchAndSaveUser(result.users, result.user), delete req.session.webauthnRegisterChallenge;
628
676
  await sendSecurityAlertEmail(req, result.user, 'webauthn_added');
629
- res.json({ success: true, credentials: result.user.webauthnCredentials });
677
+
678
+ const responseData = { success: true, credentials: result.user.webauthnCredentials };
679
+ if (newBackupCodes) responseData.backupCodes = newBackupCodes;
680
+ res.json(responseData);
630
681
  });
631
682
 
632
683
  // 获取用户凭证列表
@@ -643,26 +694,38 @@ export const accountRouter = app => {
643
694
 
644
695
  const result = getCurrentUser(req);
645
696
  if (!result) return res.status(401).json({ message: '未登录' });
646
- if (!result.user) return res.status(500).json({ message: '用户数据异常' });
647
697
 
648
- const credentials = result.user.webauthnCredentials ?? [], index = credentials.findIndex(c => c.id === credentialId);
698
+ const { user, users } = result, credentials = user.webauthnCredentials ?? [],
699
+ index = credentials.findIndex(c => c.id === credentialId);
700
+
649
701
  if (index === -1) return res.status(404).json({ message: '凭证不存在' });
702
+ credentials.splice(index, 1), user.webauthnCredentials = credentials;
650
703
 
651
- credentials.splice(index, 1);
652
- if (credentials.length === 0) result.user.webauthnEnabled = false;
653
- result.user.webauthnCredentials = credentials;
654
- touchAndSaveUser(result.users, result.user), res.json({ success: true, message: '设备已删除' });
704
+ // 如果没有凭证了,自动关闭硬件验证标志
705
+ if (credentials.length === 0) user.webauthnEnabled = false;
706
+ touchAndSaveUser(users, user), res.json({ success: true, message: '设备已删除' });
655
707
  });
656
708
 
657
709
  // 切换硬件验证启用状态
658
710
  app.post('/api/webauthn/toggle', authLimiter, async (req, res) => {
659
711
  const result = getCurrentUser(req);
660
712
  if (!result) return res.status(401).json({ message: '未登录' });
661
- if (!result.user.webauthnEnabled && result.user.webauthnCredentials.length === 0)
713
+
714
+ const newState = !result.user.webauthnEnabled;
715
+ if (newState && result.user.webauthnCredentials.length === 0)
662
716
  return res.status(400).json({ message: '没有可用的硬件凭证,请先添加设备' });
663
717
 
664
- result.user.webauthnEnabled = !result.user.webauthnEnabled, touchAndSaveUser(result.users, result.user);
665
- res.json({ success: true, enabled: result.user.webauthnEnabled });
718
+ result.user.webauthnEnabled = newState;
719
+ let newBackupCodes = null;
720
+ if (newState && (!result.user.backupCodes || result.user.backupCodes.length === 0)) {
721
+ const { plainCodes, hashedCodes } = generateBackupCodes();
722
+ result.user.backupCodes = hashedCodes, newBackupCodes = plainCodes;
723
+ }
724
+
725
+ touchAndSaveUser(result.users, result.user);
726
+ const response = { success: true, enabled: result.user.webauthnEnabled };
727
+ if (newBackupCodes) response.backupCodes = newBackupCodes;
728
+ res.json(response);
666
729
  });
667
730
 
668
731
  // 开始硬件验证登录(生成断言选项)
@@ -678,9 +741,7 @@ export const accountRouter = app => {
678
741
  options = await generateAuthenticationOptions({
679
742
  rpID,
680
743
  allowCredentials: user.webauthnCredentials.map(cred => ({
681
- id: cred.id,
682
- type: 'public-key',
683
- transports: cred.transports,
744
+ id: cred.id, type: 'public-key', transports: cred.transports,
684
745
  })),
685
746
  userVerification: 'preferred', timeout: 60000,
686
747
  });
package/dev-server.js CHANGED
@@ -150,7 +150,7 @@ const printAvailablePages = (pages, port, hotReload, useHttps, host) => {
150
150
 
151
151
  if (useHttps && (host === 'localhost' || host === '127.0.0.1')) {
152
152
  console.warn('⚠️ 警告: 使用 HTTPS 访问 localhost 会导致浏览器证书安全警告(自签名证书或证书域名不匹配)');
153
- console.warn(' 请使用 --host 参数指定与证书 CN/SAN 匹配的域名,例如: --host book.123xyz.cn');
153
+ console.warn(' 请使用 --host 参数指定与证书 CN/SAN 匹配的域名,例如: --host www.abc.com');
154
154
  }
155
155
 
156
156
  console.log('\n可访问页面:');
package/f-CHANGELOG.md CHANGED
@@ -1,10 +1,16 @@
1
1
  # 变更日志
2
+ ## [5.0.0] - 2026-06-10 10:05
3
+ ### 重大更新
4
+ - 对templates/account目录中的页面结构进行了大面积优化,如果你是老用户请注意对比迁移!!!;
5
+ > - 登录页面的具体二次验证统一由 zfa.html 处理;
6
+ > - 新增登录二次验证错误次数限制,超出将锁定24小时;
7
+ > - 备份码支持硬件使用;
8
+ > - 备份码分离为独立卡片,将由启用2FA或添加硬件时判断是否存在智能生成,并保留了重新生成按钮;
9
+ > > **更多细节请自行体验;**
10
+ - customize\account.js文件为配合上述功能实现也做了相应适配;
2
11
  ## [4.4.3] - 2026-06-05 22:16
3
12
  ### 优化
4
13
  - 模板中用户页面的移动端样式适配;
5
14
  ## [4.4.2] - 2026-05-31 15:08
6
15
  ### 修复
7
- - 修复全局中间件中 `getCurrentUser(req)` 返回 `null` 时解构报错导致服务崩溃的问题,增加空值判断并自动清理无效登录态;
8
- ## [4.4.1] - 2026-05-31 14:39
9
- ### 优化
10
- - 删除了 customize/routes.js 文件中上一次优化造成的冗余路由;
16
+ - 修复全局中间件中 `getCurrentUser(req)` 返回 `null` 时解构报错导致服务崩溃的问题,增加空值判断并自动清理无效登录态;
package/f-README.md CHANGED
@@ -4,7 +4,7 @@
4
4
 
5
5
  - **推荐方式**:使用 `import` / `export` 语法,静态分析更友好,工具链兼容性最佳;
6
6
  - **兼容方式**:Node.js ≥ 23.5.0 原生支持 `require(esm)`;22.12+ 需开启 `--experimental-require-module` 标志;
7
- - **重要**:本文档所有示例均采用 **ESM 标准**,请确保你的项目 `package.json` 中已设置 `"type": "module"`,或将脚本后缀改为 `.mjs`;
7
+ - **重要**:本文档所有示例均采用 **ESM 标准**,请确保你的项目 `package.json` 中已设置 `"type": "module"`,或将脚本后缀改为 `.mjs`;
8
8
 
9
9
  ---
10
10
 
@@ -80,7 +80,7 @@ npm i @flun/webauthn-server # 身份验证服务后端处理
80
80
  npm i @flun/webauthn-browser # 身份验证前端处理
81
81
  # 所有家族包在支持 .d.ts 提示环境下,鼠标焦点包名都有丰富的导出和使用示例提示,再也不必到处翻找使用文档;建议在VScode环境下使用;
82
82
  ```
83
- > **重要提示**:初次使用最好在空项目中执行安装,安装过程会自动复制 ESM 示例和必要文件到根目录
83
+ > **重要提示**:初次使用最好在空项目中执行安装,安装过程会自动复制 ESM 示例和必要文件到根目录
84
84
 
85
85
  ---
86
86
 
@@ -104,7 +104,7 @@ npm i @flun/webauthn-browser # 身份验证前端处理
104
104
  ```javascript
105
105
  import { startDevServer } from '@flun/html-template';
106
106
  startDevServer({ port: 7296, hotReload: true });
107
- // 如需启用 HTTPS,请参考下文“配置选项 → 启用 HTTPS”章节
107
+ // 如需启用 HTTPS,请参考下文“配置选项 → 启用 HTTPS”章节
108
108
  ```
109
109
 
110
110
  **build.js(ESM)**
@@ -121,7 +121,7 @@ initProject({ mode: 'overwrite', verbose: true });
121
121
 
122
122
  然后使用:
123
123
  ```sh
124
- # 启动开发服务器(实时预览和热重载,默认不启用登录)
124
+ # 启动开发服务器(实时预览和热重载,默认不启用登录)
125
125
  npm run dev
126
126
 
127
127
  # 启动开发服务器并启用登录系统
@@ -209,7 +209,7 @@ compile({ outputDir: 'dist' }); // 默认参数:目录名 dist;
209
209
 
210
210
  #### 方式二:使用自定义证书
211
211
 
212
- 若已有证书文件(`.key` 和 `.crt`/`.pem`),直接指定路径:
212
+ 若已有证书文件(`.key` 和 `.crt`/`.pem`),直接指定路径:
213
213
 
214
214
  ```javascript
215
215
  startDevServer({
@@ -230,9 +230,9 @@ node dev.js --https --https-key 你的私钥文件路径和文件名 --https-cer
230
230
  ```
231
231
 
232
232
  > **注意**:
233
- > - 使用 HTTPS 时,`host` 参数必须与证书中的域名(CN 或 SAN)完全一致。
234
- > - 若用 `localhost` 访问 HTTPS,浏览器会提示不安全,请按控制台警告改用正确的域名访问。
235
- > - 有关 `DnsAutoSSL.js` 的详细配置,请参考该文件内的注释或 `@flun/dns-auto-ssl` 文档。
233
+ > - 使用 HTTPS 时,`host` 参数必须与证书中的域名(CN 或 SAN)完全一致;
234
+ > - 若用 `localhost` 访问 HTTPS,浏览器会提示不安全,请按控制台警告改用正确的域名访问;
235
+ > - 有关 `DnsAutoSSL.js` 的详细配置,请参考该文件内的注释或 `@flun/dns-auto-ssl` 文档;
236
236
 
237
237
  ## 模板标签使用指南
238
238
 
@@ -244,7 +244,7 @@ node dev.js --https --https-key 你的私钥文件路径和文件名 --https-cer
244
244
  ### 标签快捷输入方式
245
245
  **HBuilder用户**:
246
246
  1. 输入 `b!` 后按 Enter
247
- 2. 输入标签名,自动创建开闭标签对
247
+ 2. 输入标签名,自动创建开闭标签对
248
248
  HBuilder自定义代码块配置(HTML和js):
249
249
  ```json
250
250
  {
@@ -261,7 +261,7 @@ HBuilder自定义代码块配置(HTML和js):
261
261
  ```
262
262
  **VS Code用户**:
263
263
  1. 安装 "html-custom-tags" 扩展
264
- 2. 输入 `[!标签名]` 后按空格,自动输出 `[~标签名]`
264
+ 2. 输入 `[!标签名]` 后按空格,自动输出 `[~标签名]`
265
265
  ***并且其还支持大部分语言语法高亮,标签特殊高亮,标签统计,标签一键全复制等功能***
266
266
 
267
267
  ---
@@ -316,7 +316,7 @@ initProject({ mode: 'skip-files', verbose: true, account: false }); // 跳过已
316
316
  4. **开发调试**:使用 `node dev.js` 运行开发服务器
317
317
  5. **编译部署**:使用 `node build.js` 编译模板
318
318
 
319
- > **注意**:编译只会打包 `customize`、`static` 目录及文件,和编译模板生成的文件。根据是否有自定义路由,动态创建 `server.js` 入口文件。
319
+ > **注意**:编译只会打包 `customize`、`static` 目录及文件,和编译模板生成的文件;根据是否有自定义路由,动态创建 `server.js` 入口文件;
320
320
 
321
321
  ---
322
322
 
@@ -487,7 +487,7 @@ export default {
487
487
  - 资源使用限制
488
488
  - 恶意代码拦截
489
489
 
490
- **用户无需额外配置**,所有安全防护在后台自动运行。
490
+ **用户无需额外配置**,所有安全防护在后台自动运行;
491
491
 
492
492
  ### 💡 使用建议
493
493
 
@@ -509,19 +509,18 @@ export default {
509
509
  [include {{dynamicPath}}]
510
510
  ```
511
511
 
512
+ ---
512
513
  ### ⚠️ 重要提醒
513
514
 
514
- 虽然我们提供了多重安全防护,但您仍需:
515
+ 虽然我们提供了多重安全防护,但您仍需:
515
516
  1. 谨慎处理用户输入数据
516
517
  2. 验证自定义函数的参数安全性
517
518
  3. 定期更新到最新版本
518
-
519
- > **安全是共同责任** - 我们负责引擎安全,您负责业务逻辑安全。
520
-
521
- 这些防护措施确保您的模板渲染过程安全可靠,让您可以专注于业务开发。
519
+ > 安装更新时不会覆盖根目录中已存在目录(customize,static和templates)
520
+ > **安全是共同责任** - 我们负责引擎安全,您负责业务逻辑安全;
521
+ 这些防护措施确保您的模板渲染过程安全可靠,让您可以专注于业务开发;
522
522
 
523
523
  ---
524
-
525
524
  ## 故障排除
526
525
 
527
526
  ### 常见问题
@@ -529,16 +528,16 @@ export default {
529
528
  2. **区块不显示**:检查开闭标签名称是否一致
530
529
  3. **热重载失效**:确认修改的是模板或静态目录中的文件
531
530
  4. **路由不工作**:检查是否正确定义了 `setupRoutes` 导出
532
- 5. **找不到函数**: 1. 检查函数名是否正确,2. 确认文件在 `customize` 目录内,3. 确认使用了 `export const functions = {...}` 语法
533
- 6. **ESM 相关错误**:检查 `package.json` 是否包含 `"type": "module"`,或启动文件是否具有 `.mjs` 扩展名。
534
- 7. **HTTPS 证书错误**:请确保 `host` 参数与证书中的域名完全匹配,且证书未被吊销或过期。开发环境可使用 `@flun/dns-auto-ssl` 自动生成受信任的证书。
531
+ 5. **找不到函数**: 1. 检查函数名是否正确,2. 确认文件在 `customize` 目录内,3. 确认使用了 `export const functions = {...}` 语法
532
+ 6. **ESM 相关错误**:检查 `package.json` 是否包含 `"type": "module"`,或启动文件是否具有 `.mjs` 扩展名;
533
+ 7. **HTTPS 证书错误**:请确保 `host` 参数与证书中的域名完全匹配,且证书未被吊销或过期;开发环境可使用 `@flun/dns-auto-ssl` 自动生成受信任的证书;
535
534
 
536
535
  ### 获取帮助
537
- 如果遇到问题,可以:
536
+ 如果遇到问题,可以:
538
537
  1. 运行恢复初始文件脚本
539
538
  2. 检查浏览器控制台错误信息
540
539
  3. 确认文件路径和名称是否正确
541
540
 
542
541
  ---
543
542
 
544
- 此模板工具包提供了从开发到生产的完整解决方案,适合各种规模的Web项目开发。
543
+ 此模板工具包提供了从开发到生产的完整解决方案,适合各种规模的Web项目开发;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@flun/html-template",
3
- "version": "4.4.3",
3
+ "version": "5.0.0",
4
4
  "description": "一个HTML模板工具包,提供开发服务器和模板编译功能,支持自定义标签和快捷输入,变量定义,包含文件引用,帮助开发者模块化处理HTML;",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",