Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
Amulnesia committed Jan 24, 2025
2 parents 62ce176 + d090876 commit 28d80cb
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 65 deletions.
16 changes: 9 additions & 7 deletions src/app/adjust/candidate.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,15 @@ import { addEvent } from "@/lib/addEvent"
import { ExcludePeriod, Period, findFreePeriods, periodsOfUsers } from "@/lib/scheduling"
import { formatDate, formatDuration } from "@/lib/utils"
import { User } from "@prisma/client"
import { useEffect, useRef, useState } from "react"
import { useRef, useState } from "react"
import { toast } from "react-toastify"
import { WeekView } from "./WeekView"
import { Course } from "@/third-party/twinte-parser-type"
import { YesNoDialog } from "@/components/ui/dialog"
import { coursePeriodsThroughWeeks } from "@/lib/course"
import { CoursePeriod } from "@/lib/course"
import { getUserCourseCodes } from "@/lib/server"
import { DEFAULT_TITLE } from "@/lib/const"

type Props = {
title: string
Expand All @@ -26,16 +27,17 @@ type Props = {
}

export default function Candidate(props: Props) {
const [isButtonActive, setIsButtonActive] = useState(false)
const [isButtonActive, setIsButtonActive] = useState(true)
const title = props.title.trim() == "" ? DEFAULT_TITLE : props.title.trim()
const [freePeriods, setFreePeriods] = useState<Period[]>([])
// const [selectedPeriod, setSelectedPeriod] = useState<Period | null>(null)
const [spannedPeriod, setSpannedPeriod] = useState<Period | null>(null)

const yesNoDialogRef = useRef<HTMLDialogElement>(null)

useEffect(() => {
setIsButtonActive(props.title.trim() !== "")
}, [props.title])
// useEffect(() => {
// setIsButtonActive(props.title.trim() !== "")
// }, [props.title])

// 自分の授業とその時間の配列
const coursePeriods: CoursePeriod[] = coursePeriodsThroughWeeks(props.courses, new Date())
Expand Down Expand Up @@ -84,7 +86,7 @@ export default function Candidate(props: Props) {
try {
await addEvent({
id: null,
summary: props.title,
summary: title,
start: period_spanned?.start,
end: period_spanned?.end,
description: null,
Expand Down Expand Up @@ -137,7 +139,7 @@ export default function Candidate(props: Props) {
onNo={() => {}}
/>
<Button onClick={handleSchedule} disabled={!isButtonActive} className="w-full">
{props.title || "-"}」の日時候補を探す
{title}」の日時候補を探す
</Button>
{freePeriods.length > 0 ? (
<WeekView
Expand Down
3 changes: 2 additions & 1 deletion src/app/adjust/client.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { formatDuration } from "@/lib/utils"
import Candidate from "./candidate"
import { Course } from "@/third-party/twinte-parser-type"
import ImportFileButton from "@/components/ImportFileButton"
import { DEFAULT_TITLE } from "@/lib/const"

// const people = [
// { id: 1, name: "HosokawaR", mail: "superkoyomi1@gmail.com" },
Expand Down Expand Up @@ -56,7 +57,7 @@ export default function SchedulePlanner(props: {
id="title"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder="予定のタイトルを入力"
placeholder={DEFAULT_TITLE}
/>
</div>
<div>
Expand Down
63 changes: 7 additions & 56 deletions src/lib/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,24 +3,14 @@ import { PrismaAdapter } from "@auth/prisma-adapter"
import { prisma } from "@/lib/prisma"

import GoogleProvider from "next-auth/providers/google"
import { googleClientId, googleClientSecret, SCOPE } from "@/lib/googleApi"
import { Account } from "@prisma/client"
import { getOrRefreshAccessToken, googleClientId, googleClientSecret, SCOPE } from "@/lib/googleApi"

declare module "next-auth" {
interface Session {
error?: "RefreshTokenError"
}
}

type Token = {
access_token: string
expires_in: number
id_token: string
refresh_token?: string
scope: string
token_type: string
}

export const authOptions: NextAuthOptions = {
adapter: PrismaAdapter(prisma),
providers: [
Expand All @@ -41,35 +31,12 @@ export const authOptions: NextAuthOptions = {
],
callbacks: {
async session({ session, user }): Promise<Session> {
const googleAccount = await prisma.account.findFirst({
where: { userId: user.id, provider: "google" },
})
if (!googleAccount) return session

if (!googleAccount.expires_at) throw new Error("Missing expires_at")
if (!googleAccount.refresh_token) throw new Error("Missing refresh_token")

if (googleAccount.expires_at * 1000 < Date.now()) {
try {
// Ref: https://accounts.google.com/.well-known/openid-configuration
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
body: new URLSearchParams({
client_id: googleClientId,
client_secret: googleClientSecret,
grant_type: "refresh_token",
refresh_token: googleAccount.refresh_token,
}),
})

const tokensOrError = await response.json()
if (!response.ok) throw tokensOrError

const newTokens = tokensOrError as Token
await updateRefreshToken(googleAccount, newTokens)
} catch (error) {
console.error("Error refreshing access_token", error)
session.error = "RefreshTokenError"
try {
await getOrRefreshAccessToken(user.id)
} catch {
return {
...session,
error: "RefreshTokenError",
}
}

Expand All @@ -81,20 +48,4 @@ export const authOptions: NextAuthOptions = {
},
}

const updateRefreshToken = async (account: Account, newTokens: Token): Promise<void> => {
await prisma.account.update({
data: {
access_token: newTokens.access_token,
expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
refresh_token: newTokens.refresh_token ?? account.refresh_token,
},
where: {
provider_providerAccountId: {
provider: "google",
providerAccountId: account.providerAccountId,
},
},
})
}

export const { handlers, auth, signIn, signOut } = NextAuth(authOptions)
1 change: 1 addition & 0 deletions src/lib/const.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export const FETCH_EVENTS_DAYS = 30
export const DEFAULT_TITLE = "スーパーこよみで追加された予定"
7 changes: 6 additions & 1 deletion src/lib/getEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { db } from "@/lib/prisma"
import { GoogleCalendar } from "./googleCalendar"
import { getServerSession } from "next-auth"
import { authOptions } from "./auth"
import { getOrRefreshAccessToken } from "@/lib/googleApi"

export async function getHostEvents() {
const session = await getServerSession(authOptions)
Expand All @@ -18,7 +19,11 @@ export async function getGuestsEvents(ids: string[]) {
const promises = ids.map(async id => {
const account = await db.findAccount(id)
if (!account?.access_token) return []
const calendar = new GoogleCalendar(account.access_token)

const accessToken = await getOrRefreshAccessToken(account.userId)
if (!accessToken) return []

const calendar = new GoogleCalendar(accessToken)
return await calendar.getBusyPeriods()
})
return Promise.all(promises)
Expand Down
65 changes: 65 additions & 0 deletions src/lib/googleApi.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { prisma } from "@/lib/prisma"
import { Account } from "@prisma/client"

const clientId = process.env.GOOGLE_CLIENT_ID!
if (!clientId) throw new Error("Missing GOOGLE_CLIENT_ID env var")

Expand All @@ -14,3 +17,65 @@ export const SCOPE = [
]

export { clientId as googleClientId, clientSecret as googleClientSecret }

type Token = {
access_token: string
expires_in: number
id_token: string
refresh_token?: string
scope: string
token_type: string
}

export const getOrRefreshAccessToken = async (userId: string): Promise<string | null> => {
const googleAccount = await prisma.account.findFirst({
where: { userId, provider: "google" },
})
if (!googleAccount) throw new Error("Missing google account")

if (!googleAccount.expires_at) throw new Error("Missing expires_at")
if (!googleAccount.refresh_token) throw new Error("Missing refresh_token")

if (googleAccount.expires_at * 1000 < Date.now()) {
try {
// Ref: https://accounts.google.com/.well-known/openid-configuration
const response = await fetch("https://oauth2.googleapis.com/token", {
method: "POST",
body: new URLSearchParams({
client_id: clientId,
client_secret: clientSecret,
grant_type: "refresh_token",
refresh_token: googleAccount.refresh_token,
}),
})

const tokensOrError = await response.json()
if (!response.ok) throw tokensOrError

const newTokens = tokensOrError as Token
await updateRefreshToken(googleAccount, newTokens)
return newTokens.access_token
} catch (error) {
console.error("Error refreshing access_token", error)
throw new Error("RefreshTokenError")
}
} else {
return googleAccount.access_token
}
}

const updateRefreshToken = async (account: Account, newTokens: Token): Promise<void> => {
await prisma.account.update({
data: {
access_token: newTokens.access_token,
expires_at: Math.floor(Date.now() / 1000 + newTokens.expires_in),
refresh_token: newTokens.refresh_token ?? account.refresh_token,
},
where: {
provider_providerAccountId: {
provider: "google",
providerAccountId: account.providerAccountId,
},
},
})
}

0 comments on commit 28d80cb

Please sign in to comment.