diff --git a/next.config.ts b/next.config.ts index 58745f9..49e5c6a 100644 --- a/next.config.ts +++ b/next.config.ts @@ -7,6 +7,10 @@ const nextConfig: NextConfig = { protocol: "https", hostname: "avatars.githubusercontent.com", }, + { + protocol: "https", + hostname: "seccdn.libravatar.org", + }, ], }, }; diff --git a/package.json b/package.json index c289f5c..8f238ba 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "@prisma/client": "^6.1.0", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", + "gitea-js": "^1.22.0", "lucide-react": "^0.469.0", "next": "15.0.3", "next-auth": "^5.0.0-beta.25", diff --git a/public/gitea.svg b/public/gitea.svg new file mode 100644 index 0000000..33293fa --- /dev/null +++ b/public/gitea.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/auth.ts b/src/auth.ts index 3fb5ddc..0151999 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -1,3 +1,4 @@ +import Gitea from "@/lib/gitea"; import NextAuth from "next-auth"; import { prisma } from "@/lib/prisma"; import GitHub from "next-auth/providers/github"; @@ -5,5 +6,5 @@ import { PrismaAdapter } from "@auth/prisma-adapter"; export const { handlers, signIn, signOut, auth } = NextAuth({ adapter: PrismaAdapter(prisma), - providers: [GitHub], + providers: [GitHub, Gitea], }); diff --git a/src/lib/gitea.ts b/src/lib/gitea.ts new file mode 100644 index 0000000..dffc59c --- /dev/null +++ b/src/lib/gitea.ts @@ -0,0 +1,76 @@ +import { Email as GiteaEmail, User } from "gitea-js"; +import { OAuthConfig, OAuthUserConfig } from "next-auth/providers"; + +export interface GiteaProfile extends User { + username?: string; +} + +export default function Gitea( + config: OAuthUserConfig & { + enterprise?: { + baseUrl?: string; + }; + } +): OAuthConfig { + const baseUrl = config?.enterprise?.baseUrl ?? "https://gitea.com"; + const apiBaseUrl = config?.enterprise?.baseUrl + ? `${config?.enterprise?.baseUrl}/api/v1` + : "https://gitea.com/api/v1"; + + return { + id: "gitea", + name: "Gitea", + type: "oauth", + authorization: { + url: `${baseUrl}/login/oauth/authorize`, + params: { scope: "read:user user:email" }, + }, + token: `${baseUrl}/login/oauth/access_token`, + userinfo: { + url: `${apiBaseUrl}/user`, + async request({ + tokens, + provider, + }: { + tokens: { access_token: string }; + provider: OAuthConfig; + }) { + const profile = await fetch(provider.userinfo?.url as URL, { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + "User-Agent": "authjs", + }, + }).then(async (res) => await res.json()); + + if (!profile.email) { + const res = await fetch(`${apiBaseUrl}/user/emails`, { + headers: { + Authorization: `Bearer ${tokens.access_token}`, + "User-Agent": "authjs", + }, + }); + + if (res.ok) { + const emails: GiteaEmail[] = await res.json(); + profile.email = (emails.find((e) => e.primary) ?? emails[0]).email; + } + } + + return profile; + }, + }, + profile(profile) { + return { + id: profile.id?.toString(), + name: profile.username ?? profile.login, + email: profile.email, + image: profile.avatar_url, + }; + }, + style: { + logo: "/gitea.svg", + brandColor: "#609926", + }, + options: config, + }; +}