171 lines
5.0 KiB
JavaScript
171 lines
5.0 KiB
JavaScript
const crypto = require("crypto");
|
||
const Sequelize = require("sequelize");
|
||
const op = Sequelize.Op;
|
||
const baseModel = require("../../middleware/baseModel");
|
||
const biz_token_secret_cipher = require("../utils/biz_token_secret_cipher");
|
||
const { normalize_for_write } = require("../utils/query_helpers");
|
||
|
||
const MAX_TOKENS_PER_USER = 5;
|
||
|
||
function hashPlainToken(plain) {
|
||
return crypto.createHash("sha256").update(plain, "utf8").digest("hex");
|
||
}
|
||
|
||
function generatePlainToken() {
|
||
return `sk-${crypto.randomBytes(24).toString("hex")}`;
|
||
}
|
||
|
||
/** 默认 Token 过期时间:一年后当日 23:59:59 */
|
||
function defaultTokenExpireAt() {
|
||
const d = new Date();
|
||
d.setFullYear(d.getFullYear() + 1);
|
||
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")} 23:59:59`;
|
||
}
|
||
|
||
/** 当前时间在 [start,end] 内且 status=active 的订阅 */
|
||
async function findActiveSubscriptionForUser(userId) {
|
||
const now = new Date();
|
||
return baseModel.biz_subscription.findOne({
|
||
where: {
|
||
user_id: userId,
|
||
status: "active",
|
||
start_time: { [op.lte]: now },
|
||
end_time: { [op.gte]: now },
|
||
},
|
||
order: [["id", "DESC"]],
|
||
});
|
||
}
|
||
|
||
async function createToken(body) {
|
||
const { user_id, token_name, expire_at, key } = body;
|
||
if (!user_id || !expire_at) throw new Error("缺少 user_id 或 expire_at");
|
||
const u = await baseModel.biz_user.findByPk(user_id);
|
||
if (!u) throw new Error("用户不存在");
|
||
if (u.status !== "active") throw new Error("用户已禁用");
|
||
|
||
const activeCount = await baseModel.biz_api_token.count({
|
||
where: { user_id, status: "active" },
|
||
});
|
||
if (activeCount >= MAX_TOKENS_PER_USER) {
|
||
throw new Error(`单用户最多 ${MAX_TOKENS_PER_USER} 个有效 Token`);
|
||
}
|
||
|
||
const sub = await findActiveSubscriptionForUser(user_id);
|
||
const plan_id = sub ? sub.plan_id : null;
|
||
|
||
const plain = generatePlainToken();
|
||
const token_hash = hashPlainToken(plain);
|
||
const secret_cipher = biz_token_secret_cipher.encrypt_plain_for_storage(plain);
|
||
|
||
const row = await baseModel.biz_api_token.create({
|
||
user_id,
|
||
plan_id,
|
||
token_name: token_name || "default",
|
||
key: key || null,
|
||
token_hash,
|
||
secret_cipher,
|
||
status: "active",
|
||
expire_at,
|
||
});
|
||
|
||
return {
|
||
row,
|
||
plain_token: plain,
|
||
warn: sub ? null : "当前无生效中的订阅,鉴权将失败",
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 管理端编辑:名称、账号 key、过期时间(不改密钥)
|
||
*/
|
||
async function updateToken(body) {
|
||
const id = body.id;
|
||
if (id == null || id === "") throw new Error("缺少 id");
|
||
const row = await baseModel.biz_api_token.findByPk(id);
|
||
if (!row) throw new Error("Token 不存在");
|
||
|
||
const payload = normalize_for_write(
|
||
baseModel.biz_api_token,
|
||
{
|
||
token_name: body.token_name,
|
||
key: body.key,
|
||
expire_at: body.expire_at,
|
||
},
|
||
{ for_create: false }
|
||
);
|
||
const patch = {};
|
||
if (payload.token_name !== undefined) patch.token_name = payload.token_name;
|
||
if (payload.key !== undefined) patch.key = payload.key;
|
||
if (payload.expire_at !== undefined) patch.expire_at = payload.expire_at;
|
||
if (Object.keys(patch).length === 0) throw new Error("没有可更新字段");
|
||
|
||
await row.update(patch);
|
||
await row.reload();
|
||
return row;
|
||
}
|
||
|
||
async function revokeToken(body) {
|
||
const id = body.id;
|
||
if (id == null) throw new Error("缺少 id");
|
||
const row = await baseModel.biz_api_token.findByPk(id);
|
||
if (!row) throw new Error("Token 不存在");
|
||
await row.update({ status: "revoked", secret_cipher: null });
|
||
return row;
|
||
}
|
||
|
||
/**
|
||
* 保留同一条 Token 记录,仅更换密钥(旧明文立即失效)
|
||
*/
|
||
async function regenerateToken(body) {
|
||
const id = body.id;
|
||
if (id == null) throw new Error("缺少 id");
|
||
const row = await baseModel.biz_api_token.findByPk(id);
|
||
if (!row) throw new Error("Token 不存在");
|
||
if (row.status !== "active") throw new Error("仅可对状态为 active 的 Token 重新生成密钥");
|
||
|
||
const u = await baseModel.biz_user.findByPk(row.user_id);
|
||
if (!u) throw new Error("用户不存在");
|
||
if (u.status !== "active") throw new Error("用户已禁用,无法轮换密钥");
|
||
|
||
const sub = await findActiveSubscriptionForUser(row.user_id);
|
||
const plan_id = sub ? sub.plan_id : null;
|
||
|
||
const plain = generatePlainToken();
|
||
const token_hash = hashPlainToken(plain);
|
||
const secret_cipher = biz_token_secret_cipher.encrypt_plain_for_storage(plain);
|
||
|
||
await row.update({
|
||
token_hash,
|
||
plan_id,
|
||
secret_cipher,
|
||
});
|
||
await row.reload();
|
||
|
||
return {
|
||
row,
|
||
plain_token: plain,
|
||
warn: sub ? null : "当前无生效中的订阅,鉴权将失败",
|
||
};
|
||
}
|
||
|
||
async function revokeAllForUser(userId) {
|
||
if (userId == null) throw new Error("缺少 user_id");
|
||
const [n] = await baseModel.biz_api_token.update(
|
||
{ status: "revoked", secret_cipher: null },
|
||
{ where: { user_id: userId, status: "active" } }
|
||
);
|
||
return n;
|
||
}
|
||
|
||
module.exports = {
|
||
hashPlainToken,
|
||
createToken,
|
||
updateToken,
|
||
regenerateToken,
|
||
revokeToken,
|
||
revokeAllForUser,
|
||
findActiveSubscriptionForUser,
|
||
defaultTokenExpireAt,
|
||
MAX_TOKENS_PER_USER,
|
||
};
|