flnxi-Wu/lib/dingtalk-login-token.ts
2026-05-25 16:20:26 +08:00

98 lines
2.4 KiB
TypeScript

import { createHash, randomBytes } from 'crypto'
import { prisma } from '@/lib/prisma'
export const DINGTALK_LOGIN_TOKEN_TTL_MS = 5 * 60 * 1000
export const DINGTALK_PROFILE_TOKEN_TTL_MS = 15 * 60 * 1000
export type DingtalkLoginTokenPurpose = 'login' | 'complete_profile'
type DingtalkTokenUser = {
id: string
email: string
username: string
role: 'ADMIN' | 'USER'
isSuperAdmin?: boolean
avatar?: string | null
isActive: boolean
}
function hashDingtalkLoginToken(token: string): string {
return createHash('sha256').update(token).digest('hex')
}
export async function createDingtalkLoginToken(
userId: string,
purpose: DingtalkLoginTokenPurpose = 'login',
ttlMs = purpose === 'complete_profile'
? DINGTALK_PROFILE_TOKEN_TTL_MS
: DINGTALK_LOGIN_TOKEN_TTL_MS
): Promise<{ token: string; expiresAt: Date }> {
const token = randomBytes(32).toString('base64url')
const expiresAt = new Date(Date.now() + ttlMs)
await prisma.dingtalkLoginToken.create({
data: {
tokenHash: hashDingtalkLoginToken(token),
userId,
purpose,
expiresAt,
},
})
return { token, expiresAt }
}
export async function consumeDingtalkLoginToken(
token: string,
purpose: DingtalkLoginTokenPurpose = 'login'
): Promise<DingtalkTokenUser | null> {
const normalizedToken = token?.trim()
if (!normalizedToken) {
return null
}
const tokenHash = hashDingtalkLoginToken(normalizedToken)
const now = new Date()
return prisma.$transaction(async (tx) => {
const loginToken = await tx.dingtalkLoginToken.findUnique({
where: { tokenHash },
include: { user: true },
})
if (
!loginToken ||
loginToken.purpose !== purpose ||
loginToken.usedAt ||
loginToken.expiresAt <= now ||
!loginToken.user
) {
return null
}
const consumed = await tx.dingtalkLoginToken.updateMany({
where: {
id: loginToken.id,
purpose,
usedAt: null,
expiresAt: { gt: now },
},
data: { usedAt: now },
})
if (consumed.count !== 1) {
return null
}
return {
id: loginToken.user.id,
email: loginToken.user.email,
username: loginToken.user.username,
role: loginToken.user.role as 'ADMIN' | 'USER',
isSuperAdmin: loginToken.user.isSuperAdmin,
avatar: loginToken.user.avatar,
isActive: loginToken.user.isActive,
}
})
}