98 lines
2.4 KiB
TypeScript
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,
|
|
}
|
|
})
|
|
}
|