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,6 +1,6 @@
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { createAuthMiddleware } from "better-auth/api";
import { createAuthEndpoint, createAuthMiddleware } from "better-auth/api";
import { apiKey } from "better-auth/plugins";
import { magicLink } from "better-auth/plugins/magic-link";
import { env } from "next-runtime-env";
@@ -11,6 +11,81 @@ import * as userRepo from "@kan/db/repository/user.repo";
import * as schema from "@kan/db/schema";
import { sendEmail } from "@kan/email";
import { createStripeClient } from "@kan/stripe";
import { socialProviderList } from "better-auth/social-providers";
export const configuredProviders = socialProviderList.reduce<
Record<
string,
{
clientId: string;
clientSecret: string;
appBundleIdentifier?: string;
tenantId?: string;
requireSelectAccount?: boolean;
clientKey?: string;
issuer?: string;
}
>
>((acc, provider) => {
const id = process.env[`${provider.toUpperCase()}_CLIENT_ID`];
const secret = process.env[`${provider.toUpperCase()}_CLIENT_SECRET`];
if (id && id.length > 0 && secret && secret.length > 0) {
acc[provider] = { clientId: id, clientSecret: secret };
}
if (
provider === "apple" &&
Object.keys(acc).includes("apple") &&
acc[provider]
) {
const bundleId =
process.env[`${provider.toUpperCase()}_APP_BUNDLE_IDENTIFIER`];
if (bundleId && bundleId.length > 0) {
acc[provider].appBundleIdentifier = bundleId;
}
}
if (
provider === "gitlab" &&
Object.keys(acc).includes("gitlab") &&
acc[provider]
) {
const issuer = process.env[`${provider.toUpperCase()}_ISSUER`];
if (issuer && issuer.length > 0) {
acc[provider].issuer = issuer;
}
}
if (
provider === "microsoft" &&
Object.keys(acc).includes("microsoft") &&
acc[provider]
) {
acc[provider].tenantId = "common";
acc[provider].requireSelectAccount = true;
}
if (
provider === "tiktok" &&
Object.keys(acc).includes("tiktok") &&
acc[provider]
) {
const key = process.env[`${provider.toUpperCase()}_CLIENT_KEY`];
if (key && key.length > 0) {
acc[provider].clientKey = key;
}
}
return acc;
}, {});
export const socialProvidersPlugin = () => ({
id: "social-providers-plugin",
endpoints: {
getSocialProviders: createAuthEndpoint(
"/social-providers",
{
method: "GET",
},
async (ctx) => ctx.json(ctx.context.socialProviders.map(p => p.name.toLowerCase())),
),
},
});
export const initAuth = (db: dbClient) => {
return betterAuth({
@@ -26,12 +101,7 @@ export const initAuth = (db: dbClient) => {
user: schema.users,
},
}),
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
},
socialProviders: configuredProviders,
user: {
additionalFields: {
stripeCustomerId: {
@@ -43,6 +113,7 @@ export const initAuth = (db: dbClient) => {
},
},
plugins: [
socialProvidersPlugin(),
// @todo: hasing is disabled due to a bug in the api key plugin
apiKey({ disableKeyHashing: true }),
magicLink({

View File

@@ -1,6 +1,25 @@
import { BetterAuthClientPlugin } from "better-auth";
import { apiKeyClient, magicLinkClient } from "better-auth/client/plugins";
import { createAuthClient } from "better-auth/react";
import { BetterFetchOption, createAuthClient } from "better-auth/react";
import { socialProvidersPlugin } from "./auth";
const socialProvidersPluginClient = {
id: "social-providers-plugin",
$InferServerPlugin: {} as ReturnType<typeof socialProvidersPlugin>,
getActions: ($fetch) => {
return {
getSocialProviders: async (fetchOptions?: BetterFetchOption) => {
const res = $fetch("/social-providers", {
method: "GET",
...fetchOptions,
});
return res.then((res) => res.data as string[]);
},
};
},
} satisfies BetterAuthClientPlugin;
export const authClient = createAuthClient({
plugins: [magicLinkClient(), apiKeyClient()],
plugins: [magicLinkClient(), apiKeyClient(), socialProvidersPluginClient],
});