这是第一个包含完整工作区的提交,也是后续「拆 SFC / 上 Vite」的回滚基线。 此前的提交均为 index.html 单文件历史快照,由 .bak 文件重建。 当前状态(提交前已实测): - index.html 内联薪酬算法已删除,改调 WHTPayroll - shared/payroll.js 为唯一薪酬实现,浏览器与 Node 共用 - 对外接口未变:aggregateMonth / isCutEdge / payDayKind 等保留同名薄包装 未入库内容见 .gitignore:依赖、Chrome 验证 profile、验证截图、 运行时 sqlite 数据与日志、时间戳备份、.trash。
66 lines
2.2 KiB
JavaScript
66 lines
2.2 KiB
JavaScript
// 认证:只用 Web Crypto(globalThis.crypto.subtle)
|
||
// 这样同一份代码在 Node 22 与 Cloudflare Workers 上都能跑,不引入任何 npm 依赖。
|
||
|
||
const enc = new TextEncoder();
|
||
|
||
export const DEFAULT_PW_ITER = 100000;
|
||
export const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000; // 30 天
|
||
|
||
function toHex(buf) {
|
||
const b = new Uint8Array(buf);
|
||
let s = '';
|
||
for (let i = 0; i < b.length; i++) s += b[i].toString(16).padStart(2, '0');
|
||
return s;
|
||
}
|
||
|
||
function fromHex(hex) {
|
||
const out = new Uint8Array(hex.length / 2);
|
||
for (let i = 0; i < out.length; i++) out[i] = parseInt(hex.substr(i * 2, 2), 16);
|
||
return out;
|
||
}
|
||
|
||
/** PBKDF2-SHA256 派生 32 字节,返回 hex */
|
||
export async function derive(password, saltHex, iterations) {
|
||
const key = await crypto.subtle.importKey('raw', enc.encode(password), 'PBKDF2', false, ['deriveBits']);
|
||
const bits = await crypto.subtle.deriveBits(
|
||
{ name: 'PBKDF2', salt: fromHex(saltHex), iterations, hash: 'SHA-256' },
|
||
key,
|
||
256
|
||
);
|
||
return toHex(bits);
|
||
}
|
||
|
||
export async function hashPassword(password, iterations = DEFAULT_PW_ITER) {
|
||
const salt = toHex(crypto.getRandomValues(new Uint8Array(16)));
|
||
const hash = await derive(password, salt, iterations);
|
||
return { pwHash: hash, pwSalt: salt, pwIter: iterations };
|
||
}
|
||
|
||
/** 定长比较,避免按字符提前返回造成的时序泄漏 */
|
||
export function safeEqualHex(a, b) {
|
||
if (typeof a !== 'string' || typeof b !== 'string' || a.length !== b.length) return false;
|
||
let diff = 0;
|
||
for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
||
return diff === 0;
|
||
}
|
||
|
||
export async function verifyPassword(password, user) {
|
||
const hash = await derive(password, user.pw_salt, user.pw_iter);
|
||
return safeEqualHex(hash, user.pw_hash);
|
||
}
|
||
|
||
/** 会话令牌:明文只回给客户端一次,库里只存 SHA-256,泄库也换不出可用 token */
|
||
export function newToken() {
|
||
return toHex(crypto.getRandomValues(new Uint8Array(32)));
|
||
}
|
||
|
||
export async function hashToken(token) {
|
||
return toHex(await crypto.subtle.digest('SHA-256', enc.encode(token)));
|
||
}
|
||
|
||
export function bearerFrom(request) {
|
||
const h = request.headers.get('authorization') || '';
|
||
const m = /^Bearer\s+([A-Za-z0-9._-]+)$/.exec(h.trim());
|
||
return m ? m[1] : null;
|
||
}
|