mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 04:19:32 +00:00
feat: notion provider (#3068)
* feat: add notion provider * chore: lint * chore: add docs for notion provider --------- Co-authored-by: Bereket Engida <86073083+Bekacru@users.noreply.github.com>
This commit is contained in:
@@ -1,15 +1,15 @@
|
||||
"use client";
|
||||
|
||||
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
|
||||
import { AsideLink } from "@/components/ui/aside-link";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { useSearchContext } from "fumadocs-ui/provider";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { contents, examples } from "./sidebar-content";
|
||||
import { ChevronDownIcon, Search } from "lucide-react";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
|
||||
import { useSearchContext } from "fumadocs-ui/provider";
|
||||
import { ChevronDownIcon, Search } from "lucide-react";
|
||||
import { usePathname, useRouter } from "next/navigation";
|
||||
import { Suspense, useEffect, useState } from "react";
|
||||
import { contents, examples } from "./sidebar-content";
|
||||
import { Badge } from "./ui/badge";
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||
|
||||
export default function ArticleLayout() {
|
||||
const [currentOpen, setCurrentOpen] = useState<number>(0);
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import {
|
||||
Book,
|
||||
CircleHelp,
|
||||
Database,
|
||||
Gauge,
|
||||
Key,
|
||||
KeyRound,
|
||||
LucideAArrowDown,
|
||||
LucideIcon,
|
||||
Mail,
|
||||
@@ -12,9 +15,6 @@ import {
|
||||
UserCircle,
|
||||
Users2,
|
||||
UserSquare2,
|
||||
Database,
|
||||
KeyRound,
|
||||
Book,
|
||||
} from "lucide-react";
|
||||
import { ReactNode, SVGProps } from "react";
|
||||
import { Icons } from "./icons";
|
||||
@@ -567,6 +567,24 @@ export const contents: Content[] = [
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Notion",
|
||||
href: "/docs/authentication/notion",
|
||||
isNew: true,
|
||||
icon: () => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1.2em"
|
||||
height="1.2em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M4.459 4.208c.746.606 1.026.56 2.428.466l13.215-.793c.28 0 .047-.28-.046-.326L17.86 1.968c-.42-.326-.981-.7-2.055-.607L3.01 2.295c-.466.046-.56.28-.374.466zm.793 3.08v13.904c0 .747.373 1.027 1.214.98l14.523-.84c.841-.046.935-.56.935-1.167V6.354c0-.606-.233-.933-.748-.887l-15.177.887c-.56.047-.747.327-.747.933zm14.337.28c.093.42 0 .84-.42.888l-.7.14v10.264c-.608.327-1.168.514-1.635.514-.748 0-.935-.234-1.495-.933l-4.577-7.186v6.952L12.21 19s0 .84-1.168.84l-3.222.186c-.093-.186 0-.653.327-.746l.84-.233V9.854L7.822 9.76c-.094-.42.14-1.026.793-1.073l3.456-.233 4.764 7.279v-6.44l-1.215-.139c-.093-.514.28-.887.747-.933zM1.936 1.035l13.31-.98c1.634-.14 2.055-.047 3.082.7l4.249 2.986c.7.513.934.653.934 1.213v16.378c0 1.026-.373 1.634-1.68 1.726l-15.458.934c-.98.047-1.448-.093-1.962-.747l-3.129-4.06c-.56-.747-.793-1.306-.793-1.96V2.667c0-.839.374-1.54 1.447-1.632z"
|
||||
/>
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: "Tiktok",
|
||||
href: "/docs/authentication/tiktok",
|
||||
|
||||
82
docs/content/docs/authentication/notion.mdx
Normal file
82
docs/content/docs/authentication/notion.mdx
Normal file
@@ -0,0 +1,82 @@
|
||||
---
|
||||
title: Notion
|
||||
description: Notion provider setup and usage.
|
||||
---
|
||||
|
||||
<Steps>
|
||||
<Step>
|
||||
### Get your Notion credentials
|
||||
To use Notion as a social provider, you need to get your Notion OAuth credentials. You can get them by creating a new integration in the [Notion Developers Portal](https://www.notion.so/my-integrations).
|
||||
|
||||
In the Notion integration settings > OAuth Domain & URIs, make sure to set the redirect URL to `http://localhost:3000/api/auth/callback/notion` for local development. For production, make sure to set the redirect URL as your application domain, e.g. `https://example.com/api/auth/callback/notion`. If you change the base path of the auth routes, you should update the redirect URL accordingly.
|
||||
|
||||
<Callout>
|
||||
Make sure your Notion integration has the appropriate capabilities enabled. For user authentication, you'll need the "Read user information including email addresses" capability.
|
||||
</Callout>
|
||||
</Step>
|
||||
|
||||
<Step>
|
||||
### Configure the provider
|
||||
To configure the provider, you need to pass the `clientId` and `clientSecret` to `socialProviders.notion` in your auth configuration.
|
||||
|
||||
```ts title="auth.ts"
|
||||
import { betterAuth } from "better-auth"
|
||||
|
||||
export const auth = betterAuth({
|
||||
socialProviders: {
|
||||
notion: { // [!code highlight]
|
||||
clientId: process.env.NOTION_CLIENT_ID as string, // [!code highlight]
|
||||
clientSecret: process.env.NOTION_CLIENT_SECRET as string, // [!code highlight]
|
||||
}, // [!code highlight]
|
||||
},
|
||||
})
|
||||
```
|
||||
</Step>
|
||||
</Steps>
|
||||
|
||||
## Usage
|
||||
|
||||
### Sign In with Notion
|
||||
|
||||
To sign in with Notion, you can use the `signIn.social` function provided by the client. The `signIn` function takes an object with the following properties:
|
||||
- `provider`: The provider to use. It should be set to `notion`.
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
import { createAuthClient } from "better-auth/client"
|
||||
const authClient = createAuthClient()
|
||||
|
||||
const signIn = async () => {
|
||||
const data = await authClient.signIn.social({
|
||||
provider: "notion"
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### Notion Integration Types
|
||||
|
||||
Notion supports different integration types. When creating your integration, you can choose between:
|
||||
|
||||
- **Public integrations**: Can be installed by any Notion workspace
|
||||
- **Internal integrations**: Limited to your own workspace
|
||||
|
||||
For most authentication use cases, you'll want to create a public integration to allow users from different workspaces to sign in.
|
||||
|
||||
### Requesting Additional Notion Scopes
|
||||
|
||||
If your application needs additional Notion capabilities after the user has already signed up, you can request them using the `linkSocial` method with the same Notion provider and additional scopes.
|
||||
|
||||
```ts title="auth-client.ts"
|
||||
const requestNotionAccess = async () => {
|
||||
await authClient.linkSocial({
|
||||
provider: "notion",
|
||||
// Notion automatically provides access based on integration capabilities
|
||||
});
|
||||
};
|
||||
|
||||
// Example usage in a React component
|
||||
return <button onClick={requestNotionAccess}>Connect Notion Workspace</button>;
|
||||
```
|
||||
|
||||
<Callout>
|
||||
After authentication, you can use the access token to interact with the Notion API to read and write pages, databases, and other content that the user has granted access to.
|
||||
</Callout>
|
||||
@@ -1,12 +1,21 @@
|
||||
import { z } from "zod";
|
||||
import type { Prettify } from "../types/helper";
|
||||
import { apple } from "./apple";
|
||||
import { discord } from "./discord";
|
||||
import { dropbox } from "./dropbox";
|
||||
import { facebook } from "./facebook";
|
||||
import { github } from "./github";
|
||||
import { gitlab } from "./gitlab";
|
||||
import { google } from "./google";
|
||||
import { kick } from "./kick";
|
||||
import { linkedin } from "./linkedin";
|
||||
import { huggingface } from "./huggingface";
|
||||
import { microsoft } from "./microsoft-entra-id";
|
||||
import { notion } from "./notion";
|
||||
import { reddit } from "./reddit";
|
||||
import { roblox } from "./roblox";
|
||||
import { spotify } from "./spotify";
|
||||
import { tiktok } from "./tiktok";
|
||||
import { twitch } from "./twitch";
|
||||
import { twitter } from "./twitter";
|
||||
import { dropbox } from "./dropbox";
|
||||
@@ -18,7 +27,6 @@ import { reddit } from "./reddit";
|
||||
import { roblox } from "./roblox";
|
||||
import { z } from "zod";
|
||||
import { vk } from "./vk";
|
||||
import { kick } from "./kick";
|
||||
import { zoom } from "./zoom";
|
||||
export const socialProviders = {
|
||||
apple,
|
||||
@@ -41,6 +49,7 @@ export const socialProviders = {
|
||||
roblox,
|
||||
vk,
|
||||
zoom,
|
||||
notion,
|
||||
};
|
||||
|
||||
export const socialProviderList = Object.keys(socialProviders) as [
|
||||
@@ -62,22 +71,25 @@ export type SocialProviders = {
|
||||
>;
|
||||
};
|
||||
|
||||
export * from "./github";
|
||||
export * from "./google";
|
||||
export * from "./apple";
|
||||
export * from "./microsoft-entra-id";
|
||||
export * from "./discord";
|
||||
export * from "./spotify";
|
||||
export * from "./twitch";
|
||||
export * from "./facebook";
|
||||
export * from "./twitter";
|
||||
export * from "./dropbox";
|
||||
export * from "./facebook";
|
||||
export * from "./github";
|
||||
export * from "./linear";
|
||||
export * from "./linkedin";
|
||||
export * from "./gitlab";
|
||||
export * from "./tiktok";
|
||||
export * from "./google";
|
||||
export * from "./kick";
|
||||
export * from "./linkedin";
|
||||
export * from "./microsoft-entra-id";
|
||||
export * from "./notion";
|
||||
export * from "./reddit";
|
||||
export * from "./roblox";
|
||||
export * from "./spotify";
|
||||
export * from "./tiktok";
|
||||
export * from "./twitch";
|
||||
export * from "./twitter";
|
||||
export * from "./vk";
|
||||
export * from "./zoom";
|
||||
export * from "./kick";
|
||||
|
||||
103
packages/better-auth/src/social-providers/notion.ts
Normal file
103
packages/better-auth/src/social-providers/notion.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import { betterFetch } from "@better-fetch/fetch";
|
||||
import type { OAuthProvider, ProviderOptions } from "../oauth2";
|
||||
import {
|
||||
createAuthorizationURL,
|
||||
refreshAccessToken,
|
||||
validateAuthorizationCode,
|
||||
} from "../oauth2";
|
||||
|
||||
export interface NotionProfile {
|
||||
object: "user";
|
||||
id: string;
|
||||
type: "person" | "bot";
|
||||
name?: string;
|
||||
avatar_url?: string;
|
||||
person?: {
|
||||
email?: string;
|
||||
};
|
||||
bot?: {
|
||||
owner: {
|
||||
type: "workspace" | "user";
|
||||
};
|
||||
workspace_name?: string;
|
||||
};
|
||||
}
|
||||
|
||||
export interface NotionOptions extends ProviderOptions<NotionProfile> {}
|
||||
|
||||
export const notion = (options: NotionOptions) => {
|
||||
const tokenEndpoint = "https://api.notion.com/v1/oauth/token";
|
||||
return {
|
||||
id: "notion",
|
||||
name: "Notion",
|
||||
createAuthorizationURL({ state, scopes, loginHint, redirectURI }) {
|
||||
const _scopes: string[] = options.disableDefaultScope ? [] : [];
|
||||
options.scope && _scopes.push(...options.scope);
|
||||
scopes && _scopes.push(...scopes);
|
||||
return createAuthorizationURL({
|
||||
id: "notion",
|
||||
options,
|
||||
authorizationEndpoint: "https://api.notion.com/v1/oauth/authorize",
|
||||
scopes: _scopes,
|
||||
state,
|
||||
redirectURI,
|
||||
loginHint,
|
||||
additionalParams: {
|
||||
owner: "user",
|
||||
},
|
||||
});
|
||||
},
|
||||
validateAuthorizationCode: async ({ code, redirectURI }) => {
|
||||
return validateAuthorizationCode({
|
||||
code,
|
||||
redirectURI,
|
||||
options,
|
||||
tokenEndpoint,
|
||||
});
|
||||
},
|
||||
refreshAccessToken: options.refreshAccessToken
|
||||
? options.refreshAccessToken
|
||||
: async (refreshToken) => {
|
||||
return refreshAccessToken({
|
||||
refreshToken,
|
||||
options: {
|
||||
clientId: options.clientId,
|
||||
clientKey: options.clientKey,
|
||||
clientSecret: options.clientSecret,
|
||||
},
|
||||
tokenEndpoint,
|
||||
});
|
||||
},
|
||||
async getUserInfo(token) {
|
||||
if (options.getUserInfo) {
|
||||
return options.getUserInfo(token);
|
||||
}
|
||||
const { data: profile, error } = await betterFetch<NotionProfile>(
|
||||
"https://api.notion.com/v1/users/me",
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token.accessToken}`,
|
||||
"Notion-Version": "2022-06-28",
|
||||
},
|
||||
},
|
||||
);
|
||||
if (error) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const userMap = await options.mapProfileToUser?.(profile);
|
||||
return {
|
||||
user: {
|
||||
id: profile.id,
|
||||
name: profile.name || "Notion User",
|
||||
email: profile.person?.email || null,
|
||||
image: profile.avatar_url,
|
||||
emailVerified: !!profile.person?.email,
|
||||
...userMap,
|
||||
},
|
||||
data: profile,
|
||||
};
|
||||
},
|
||||
options,
|
||||
} satisfies OAuthProvider<NotionProfile>;
|
||||
};
|
||||
Reference in New Issue
Block a user