add google oauth login
ci/woodpecker/push/woodpecker Pipeline was successful

This commit is contained in:
2026-07-06 11:45:35 +09:00
parent 7c67e53b1e
commit ea5eac64c7
5 changed files with 421 additions and 1 deletions
+121
View File
@@ -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)}`