修复认证验证码投递与登录入口体验 05-27-oa-auth-delivery-wecom-entry-fix
这个提交包含在:
+80
-7
@@ -8,7 +8,7 @@ const { validatePassword } = require("../utils/passwordPolicy");
|
||||
const { signPayload, verifyToken } = require("../utils/signedUrl");
|
||||
const { sendData } = require("../utils/reply");
|
||||
const { createNotifications } = require("../services/notifications");
|
||||
const { sendNotificationEmailSafe } = require("../services/email");
|
||||
const { resolveEmailDeliveryStatus, sendNotificationEmailSafe } = require("../services/email");
|
||||
const { generators, getOidcClient } = require("../services/oidc");
|
||||
|
||||
const PENDING_WECOM_BIND_COOKIE_NAME = "wecom_login_bind";
|
||||
@@ -245,6 +245,53 @@ function emailLoginCodeError() {
|
||||
});
|
||||
}
|
||||
|
||||
function emailDeliveryUnavailableError() {
|
||||
return new AppError({
|
||||
code: "SYS_001",
|
||||
message: "邮件服务暂未启用",
|
||||
statusCode: 503,
|
||||
details: { email: "delivery_not_configured" },
|
||||
});
|
||||
}
|
||||
|
||||
function assertEmailDeliveryReady(config) {
|
||||
const status = resolveEmailDeliveryStatus(config);
|
||||
if (!status.ready) {
|
||||
throw emailDeliveryUnavailableError();
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
function getPart(parts, type) {
|
||||
return parts.find((part) => part.type === type)?.value ?? "";
|
||||
}
|
||||
|
||||
function formatChinaDateTime(date) {
|
||||
const parts = new Intl.DateTimeFormat("zh-CN", {
|
||||
timeZone: "Asia/Shanghai",
|
||||
year: "numeric",
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hourCycle: "h23",
|
||||
}).formatToParts(date);
|
||||
|
||||
return `${getPart(parts, "year")}年${getPart(parts, "month")}月${getPart(parts, "day")}日 ${getPart(parts, "hour")}:${getPart(parts, "minute")}(北京时间)`;
|
||||
}
|
||||
|
||||
function buildAuthVerificationEmail({ code, expiresAt, purpose }) {
|
||||
const subject =
|
||||
purpose === "password_reset"
|
||||
? "【海音达OA】密码重置验证码"
|
||||
: "【海音达OA】登录验证码";
|
||||
|
||||
return {
|
||||
subject,
|
||||
body: `验证码:${code}\n有效期至:${formatChinaDateTime(expiresAt)}\n如非本人操作,请忽略并联系管理员。`,
|
||||
};
|
||||
}
|
||||
|
||||
async function createRefreshSession(tx, { employeeId, refreshTokenHash, deviceFingerprint, request, now, expiresAt }) {
|
||||
const activeSessions = await tx.authSession.findMany({
|
||||
where: {
|
||||
@@ -303,6 +350,22 @@ async function authRoutes(fastify) {
|
||||
: undefined;
|
||||
const accessTokenExpiresIn = parseAccessTokenTtlSeconds(fastify.config.accessTokenTtl);
|
||||
|
||||
fastify.get("/capabilities", async (request, reply) => {
|
||||
const emailDelivery = resolveEmailDeliveryStatus(fastify.config);
|
||||
return sendData(reply, request, {
|
||||
emailDelivery: {
|
||||
ready: emailDelivery.ready,
|
||||
statusReasons: emailDelivery.reasons,
|
||||
},
|
||||
emailLoginCode: {
|
||||
enabled: emailDelivery.ready,
|
||||
},
|
||||
passwordReset: {
|
||||
enabled: emailDelivery.ready,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
fastify.post("/login", { preHandler: authRateLimit }, async (request, reply) => {
|
||||
const parsed = loginBodySchema.safeParse(request.body);
|
||||
if (!parsed.success) {
|
||||
@@ -574,6 +637,8 @@ async function authRoutes(fastify) {
|
||||
});
|
||||
}
|
||||
|
||||
assertEmailDeliveryReady(fastify.config);
|
||||
|
||||
const { email } = parsed.data;
|
||||
const employee = await fastify.prisma.employee.findFirst({
|
||||
where: { email },
|
||||
@@ -588,6 +653,7 @@ async function authRoutes(fastify) {
|
||||
const codeHash = hashEmailLoginCode(employee.id, code, fastify.config.jwtSecret);
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + fastify.config.emailLoginCodeTtlSeconds * 1000);
|
||||
const emailMessage = buildAuthVerificationEmail({ code, expiresAt, purpose: "login" });
|
||||
|
||||
await fastify.prisma.emailLoginCode.create({
|
||||
data: {
|
||||
@@ -616,8 +682,8 @@ async function authRoutes(fastify) {
|
||||
requestId: request.id,
|
||||
type: "auth.email_login_code_requested",
|
||||
recipientEmployeeIds: [employee.id],
|
||||
subject: "【安全】OA 登录验证码",
|
||||
body: `你的 OA 登录验证码为:${code}\n有效期至:${expiresAt.toISOString()}\n如非本人操作,请忽略并联系管理员核查账号安全。`,
|
||||
subject: emailMessage.subject,
|
||||
body: emailMessage.body,
|
||||
});
|
||||
|
||||
return sendData(reply, request, {});
|
||||
@@ -1355,7 +1421,7 @@ async function authRoutes(fastify) {
|
||||
});
|
||||
|
||||
const forgotPasswordBodySchema = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.string().trim().email().transform((value) => value.toLowerCase()),
|
||||
});
|
||||
|
||||
fastify.post("/forgot-password", { preHandler: authRateLimit }, async (request, reply) => {
|
||||
@@ -1369,6 +1435,8 @@ async function authRoutes(fastify) {
|
||||
});
|
||||
}
|
||||
|
||||
assertEmailDeliveryReady(fastify.config);
|
||||
|
||||
const { email } = parsed.data;
|
||||
const employee = await fastify.prisma.employee.findFirst({
|
||||
where: { email },
|
||||
@@ -1383,6 +1451,7 @@ async function authRoutes(fastify) {
|
||||
const codeHash = sha256Hex(code);
|
||||
const now = new Date();
|
||||
const expiresAt = new Date(now.getTime() + fastify.config.passwordResetCodeTtlSeconds * 1000);
|
||||
const emailMessage = buildAuthVerificationEmail({ code, expiresAt, purpose: "password_reset" });
|
||||
|
||||
await fastify.prisma.passwordResetCode.create({
|
||||
data: {
|
||||
@@ -1427,8 +1496,8 @@ async function authRoutes(fastify) {
|
||||
requestId: request.id,
|
||||
type: "auth.password_reset_requested",
|
||||
recipientEmployeeIds: [employee.id],
|
||||
subject: "【安全】密码重置验证码",
|
||||
body: `你的密码重置验证码为:${code}\n有效期至:${expiresAt.toISOString()}\n如非本人操作,请忽略并联系管理员核查账号安全。`,
|
||||
subject: emailMessage.subject,
|
||||
body: emailMessage.body,
|
||||
});
|
||||
|
||||
if (fastify.config.devShowPasswordResetCode) {
|
||||
@@ -1442,7 +1511,7 @@ async function authRoutes(fastify) {
|
||||
});
|
||||
|
||||
const resetPasswordBodySchema = z.object({
|
||||
email: z.string().email(),
|
||||
email: z.string().trim().email().transform((value) => value.toLowerCase()),
|
||||
code: z.string().length(6),
|
||||
newPassword: z.string().min(12),
|
||||
});
|
||||
@@ -1577,3 +1646,7 @@ async function authRoutes(fastify) {
|
||||
}
|
||||
|
||||
module.exports = authRoutes;
|
||||
module.exports._test = {
|
||||
buildAuthVerificationEmail,
|
||||
formatChinaDateTime,
|
||||
};
|
||||
|
||||
在新工单中引用
屏蔽一个用户