This commit is contained in:
@@ -197,6 +197,89 @@ export async function createUser(input: { email: string; password: string; name?
|
||||
return toAuthUser(user)
|
||||
}
|
||||
|
||||
export async function findOrCreateOAuthUser(input: {
|
||||
provider: string
|
||||
providerUserId: string
|
||||
email: string
|
||||
name?: string | null
|
||||
}): Promise<AuthUser> {
|
||||
const provider = input.provider.toLowerCase()
|
||||
const email = input.email.toLowerCase()
|
||||
|
||||
const oauthAccount = await prisma.userOauthAccount.findUnique({
|
||||
where: {
|
||||
provider_providerUserId: {
|
||||
provider,
|
||||
providerUserId: input.providerUserId
|
||||
}
|
||||
},
|
||||
include: {
|
||||
user: {
|
||||
select: userSelect()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if (oauthAccount?.user) return toAuthUser(oauthAccount.user)
|
||||
|
||||
const existingUser = await prisma.user.findUnique({
|
||||
where: { email },
|
||||
select: { id: true }
|
||||
})
|
||||
|
||||
if (existingUser) {
|
||||
const user = await prisma.user.update({
|
||||
where: { id: existingUser.id },
|
||||
data: {
|
||||
oauthAccounts: {
|
||||
create: {
|
||||
provider,
|
||||
providerUserId: input.providerUserId,
|
||||
email
|
||||
}
|
||||
}
|
||||
},
|
||||
select: userSelect()
|
||||
})
|
||||
|
||||
return toAuthUser(user)
|
||||
}
|
||||
|
||||
const freeLicense = await prisma.license.findUnique({
|
||||
where: { code: 'FREE' },
|
||||
select: { defaultDurationDays: true }
|
||||
})
|
||||
const licenseStartsAt = new Date()
|
||||
const licenseExpiresAt = calculateLicenseExpiresAt(licenseStartsAt, freeLicense?.defaultDurationDays)
|
||||
|
||||
const user = await prisma.user.create({
|
||||
data: {
|
||||
email,
|
||||
passwordHash: await hashPassword(createOpaqueToken()),
|
||||
name: input.name?.trim() || null,
|
||||
oauthAccounts: {
|
||||
create: {
|
||||
provider,
|
||||
providerUserId: input.providerUserId,
|
||||
email
|
||||
}
|
||||
},
|
||||
userLicenses: {
|
||||
create: {
|
||||
startsAt: licenseStartsAt,
|
||||
expiresAt: licenseExpiresAt,
|
||||
license: {
|
||||
connect: { code: 'FREE' }
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
select: userSelect()
|
||||
})
|
||||
|
||||
return toAuthUser(user)
|
||||
}
|
||||
|
||||
function calculateLicenseExpiresAt(startsAt: Date, durationDays: number | null | undefined): Date | null {
|
||||
if (!durationDays) return null
|
||||
|
||||
@@ -503,6 +586,42 @@ export async function rotateRefreshToken(refreshToken: string): Promise<AuthUser
|
||||
return findUserById(token.userId)
|
||||
}
|
||||
|
||||
export async function createOAuthState(redirectUri: string): Promise<string> {
|
||||
const state = createOpaqueToken()
|
||||
await redis.set(oauthStateKey(state), JSON.stringify({ redirectUri }), 'EX', 10 * 60)
|
||||
return state
|
||||
}
|
||||
|
||||
export async function consumeOAuthState(state: string): Promise<{ redirectUri: string } | null> {
|
||||
const tokenJson = await redis.send('GETDEL', [oauthStateKey(state)])
|
||||
if (!tokenJson) return null
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(String(tokenJson)) as { redirectUri?: unknown }
|
||||
return typeof payload.redirectUri === 'string' ? { redirectUri: payload.redirectUri } : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function createOAuthLoginToken(userId: string): Promise<string> {
|
||||
const token = createOpaqueToken()
|
||||
await redis.set(oauthLoginTokenKey(token), JSON.stringify({ userId }), 'EX', config.oauthLoginTokenTtlSeconds)
|
||||
return token
|
||||
}
|
||||
|
||||
export async function exchangeOAuthLoginToken(loginToken: string): Promise<AuthUser | null> {
|
||||
const tokenJson = await redis.send('GETDEL', [oauthLoginTokenKey(loginToken)])
|
||||
if (!tokenJson) return null
|
||||
|
||||
try {
|
||||
const payload = JSON.parse(String(tokenJson)) as { userId?: unknown }
|
||||
return typeof payload.userId === 'string' ? findUserById(payload.userId) : null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function revokeRefreshToken(refreshToken: string): Promise<void> {
|
||||
await Promise.all([
|
||||
redis.del(refreshTokenKey(refreshToken)),
|
||||
@@ -549,6 +668,8 @@ export async function exchangeHandoffToken(handoffToken: string, audienceApp?: s
|
||||
}
|
||||
|
||||
const refreshTokenKey = (token: string): string => `auth:refresh:${hashToken(token)}`
|
||||
const oauthStateKey = (state: string): string => `auth:oauth:state:${hashToken(state)}`
|
||||
const oauthLoginTokenKey = (token: string): string => `auth:oauth:login:${hashToken(token)}`
|
||||
|
||||
const handoffTokenKey = (token: string): string => `auth:handoff:${hashToken(token)}`
|
||||
|
||||
|
||||
Reference in New Issue
Block a user