mirror of
https://github.com/LukeHagar/better-auth.git
synced 2025-12-10 12:27:44 +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";
|
"use client";
|
||||||
|
|
||||||
import { AnimatePresence, motion, MotionConfig } from "framer-motion";
|
|
||||||
import { AsideLink } from "@/components/ui/aside-link";
|
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 { 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 { Badge } from "./ui/badge";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger } from "./ui/select";
|
||||||
|
|
||||||
export default function ArticleLayout() {
|
export default function ArticleLayout() {
|
||||||
const [currentOpen, setCurrentOpen] = useState<number>(0);
|
const [currentOpen, setCurrentOpen] = useState<number>(0);
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import {
|
import {
|
||||||
|
Book,
|
||||||
CircleHelp,
|
CircleHelp,
|
||||||
|
Database,
|
||||||
Gauge,
|
Gauge,
|
||||||
Key,
|
Key,
|
||||||
|
KeyRound,
|
||||||
LucideAArrowDown,
|
LucideAArrowDown,
|
||||||
LucideIcon,
|
LucideIcon,
|
||||||
Mail,
|
Mail,
|
||||||
@@ -12,9 +15,6 @@ import {
|
|||||||
UserCircle,
|
UserCircle,
|
||||||
Users2,
|
Users2,
|
||||||
UserSquare2,
|
UserSquare2,
|
||||||
Database,
|
|
||||||
KeyRound,
|
|
||||||
Book,
|
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { ReactNode, SVGProps } from "react";
|
import { ReactNode, SVGProps } from "react";
|
||||||
import { Icons } from "./icons";
|
import { Icons } from "./icons";
|
||||||
@@ -567,6 +567,24 @@ export const contents: Content[] = [
|
|||||||
</svg>
|
</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",
|
title: "Tiktok",
|
||||||
href: "/docs/authentication/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 type { Prettify } from "../types/helper";
|
||||||
import { apple } from "./apple";
|
import { apple } from "./apple";
|
||||||
import { discord } from "./discord";
|
import { discord } from "./discord";
|
||||||
|
import { dropbox } from "./dropbox";
|
||||||
import { facebook } from "./facebook";
|
import { facebook } from "./facebook";
|
||||||
import { github } from "./github";
|
import { github } from "./github";
|
||||||
|
import { gitlab } from "./gitlab";
|
||||||
import { google } from "./google";
|
import { google } from "./google";
|
||||||
|
import { kick } from "./kick";
|
||||||
|
import { linkedin } from "./linkedin";
|
||||||
import { huggingface } from "./huggingface";
|
import { huggingface } from "./huggingface";
|
||||||
import { microsoft } from "./microsoft-entra-id";
|
import { microsoft } from "./microsoft-entra-id";
|
||||||
|
import { notion } from "./notion";
|
||||||
|
import { reddit } from "./reddit";
|
||||||
|
import { roblox } from "./roblox";
|
||||||
import { spotify } from "./spotify";
|
import { spotify } from "./spotify";
|
||||||
|
import { tiktok } from "./tiktok";
|
||||||
import { twitch } from "./twitch";
|
import { twitch } from "./twitch";
|
||||||
import { twitter } from "./twitter";
|
import { twitter } from "./twitter";
|
||||||
import { dropbox } from "./dropbox";
|
import { dropbox } from "./dropbox";
|
||||||
@@ -18,7 +27,6 @@ import { reddit } from "./reddit";
|
|||||||
import { roblox } from "./roblox";
|
import { roblox } from "./roblox";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { vk } from "./vk";
|
import { vk } from "./vk";
|
||||||
import { kick } from "./kick";
|
|
||||||
import { zoom } from "./zoom";
|
import { zoom } from "./zoom";
|
||||||
export const socialProviders = {
|
export const socialProviders = {
|
||||||
apple,
|
apple,
|
||||||
@@ -41,6 +49,7 @@ export const socialProviders = {
|
|||||||
roblox,
|
roblox,
|
||||||
vk,
|
vk,
|
||||||
zoom,
|
zoom,
|
||||||
|
notion,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const socialProviderList = Object.keys(socialProviders) as [
|
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 "./apple";
|
||||||
export * from "./microsoft-entra-id";
|
|
||||||
export * from "./discord";
|
export * from "./discord";
|
||||||
export * from "./spotify";
|
|
||||||
export * from "./twitch";
|
|
||||||
export * from "./facebook";
|
|
||||||
export * from "./twitter";
|
|
||||||
export * from "./dropbox";
|
export * from "./dropbox";
|
||||||
|
export * from "./facebook";
|
||||||
|
export * from "./github";
|
||||||
export * from "./linear";
|
export * from "./linear";
|
||||||
export * from "./linkedin";
|
export * from "./linkedin";
|
||||||
export * from "./gitlab";
|
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 "./reddit";
|
||||||
export * from "./roblox";
|
export * from "./roblox";
|
||||||
|
export * from "./spotify";
|
||||||
|
export * from "./tiktok";
|
||||||
|
export * from "./twitch";
|
||||||
|
export * from "./twitter";
|
||||||
export * from "./vk";
|
export * from "./vk";
|
||||||
export * from "./zoom";
|
export * from "./zoom";
|
||||||
export * from "./kick";
|
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