feat(auth): added discord & github authentication methods (#18)

* feat(auth): added discord & github authentication methods

* chore(format): formatted edited files

* docs: add Discord and GitHub OAuth environment variables to README

* feat: add Discord and GitHub OAuth client credentials to environment config

* refactor: migrate social provider types to better-auth package

* feat: return configured social provider names from auth endpoint

* refactor: simplify socialProvidersPlugin by removing nested function and condensing return
This commit is contained in:
LovelessCodes
2025-06-05 13:00:07 +02:00
committed by GitHub
parent 4b7f58aeeb
commit 6eb17529f1
9 changed files with 219 additions and 44 deletions

View File

@@ -1,8 +1,10 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { useQuery } from "@tanstack/react-query";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { FaDiscord, FaGithub, FaGoogle } from "react-icons/fa";
import { z } from "zod";
import type { SocialProvider } from "better-auth/social-providers";
import { authClient } from "@kan/auth/client";
@@ -20,8 +22,9 @@ interface AuthProps {
const EmailSchema = z.object({ email: z.string().email() });
export function Auth({ setIsMagicLinkSent }: AuthProps) {
const [isLoginWithGooglePending, setIsLoginWithGooglePending] =
useState(false);
const [isLoginWithProviderPending, setIsLoginWithProviderPending] = useState<
null | SocialProvider
>(null);
const [isLoginWithEmailPending, setIsLoginWithEmailPending] = useState(false);
const [loginError, setLoginError] = useState<string | null>(null);
@@ -33,6 +36,11 @@ export function Auth({ setIsMagicLinkSent }: AuthProps) {
resolver: zodResolver(EmailSchema),
});
const { data: socialProviders } = useQuery({
queryKey: ["social_providers"],
queryFn: () => authClient.getSocialProviders(),
});
const handleLoginWithEmail = async (email: string) => {
setIsLoginWithEmailPending(true);
setLoginError(null);
@@ -52,18 +60,21 @@ export function Auth({ setIsMagicLinkSent }: AuthProps) {
}
};
const handleLoginWithGoogle = async () => {
setIsLoginWithGooglePending(true);
const handleLoginWithProvider = async (
provider: SocialProvider) => {
setIsLoginWithProviderPending(provider);
setLoginError(null);
const { error } = await authClient.signIn.social({
provider: "google",
provider,
callbackURL: "/boards",
});
setIsLoginWithGooglePending(false);
setIsLoginWithProviderPending(null);
if (error) {
setLoginError("Failed to login with Google. Please try again.");
setLoginError(
`Failed to login with ${provider.at(0)?.toUpperCase() + provider.slice(1)}. Please try again.`,
);
}
};
@@ -73,23 +84,53 @@ export function Auth({ setIsMagicLinkSent }: AuthProps) {
return (
<div className="space-y-6">
<div>
<Button
onClick={handleLoginWithGoogle}
isLoading={isLoginWithGooglePending}
iconLeft={<FaGoogle />}
fullWidth
size="lg"
>
Continue with Google
</Button>
</div>
<form onSubmit={handleSubmit(onSubmit)}>
<div className="mb-[1.5rem] flex w-full items-center gap-4">
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
<span className="text-sm text-light-900 dark:text-dark-900">or</span>
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
{socialProviders?.length !== 0 && (
<div className="space-y-2">
{socialProviders?.includes("google") && (
<Button
onClick={() => handleLoginWithProvider("google")}
isLoading={isLoginWithProviderPending === "google"}
iconLeft={<FaGoogle />}
fullWidth
size="lg"
>
Continue with Google
</Button>
)}
{socialProviders?.includes("github") && (
<Button
onClick={() => handleLoginWithProvider("github")}
isLoading={isLoginWithProviderPending === "github"}
iconLeft={<FaGithub />}
fullWidth
size="lg"
>
Continue with GitHub
</Button>
)}
{socialProviders?.includes("discord") && (
<Button
onClick={() => handleLoginWithProvider("discord")}
isLoading={isLoginWithProviderPending === "discord"}
iconLeft={<FaDiscord />}
fullWidth
size="lg"
>
Continue with Discord
</Button>
)}
</div>
)}
<form onSubmit={handleSubmit(onSubmit)}>
{socialProviders?.length !== 0 && (
<div className="mb-[1.5rem] flex w-full items-center gap-4">
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
<span className="text-sm text-light-900 dark:text-dark-900">
or
</span>
<div className="h-[1px] w-full bg-light-600 dark:bg-dark-600" />
</div>
)}
<Input
{...register("email", { required: true })}
placeholder="Enter your email address"

View File

@@ -21,6 +21,10 @@ export const env = createEnv({
STRIPE_SECRET_KEY: z.string().optional(),
GOOGLE_CLIENT_ID: z.string().optional(),
GOOGLE_CLIENT_SECRET: z.string().optional(),
DISCORD_CLIENT_ID: z.string().optional(),
DISCORD_CLIENT_SECRET: z.string().optional(),
GITHUB_CLIENT_ID: z.string().optional(),
GITHUB_CLIENT_SECRET: z.string().optional(),
S3_ACCESS_KEY_ID: z.string().optional(),
S3_SECRET_ACCESS_KEY: z.string().optional(),
S3_REGION: z.string().optional(),