Notion-Auth

This commit is contained in:
tangly1024.com
2024-07-10 09:19:59 +08:00
parent da90c17b3a
commit ccf0c18017
11 changed files with 1247 additions and 406 deletions

View File

@@ -125,6 +125,7 @@ const BLOG = {
'/[prefix]': 'LayoutSlug', '/[prefix]': 'LayoutSlug',
'/[prefix]/[slug]': 'LayoutSlug', '/[prefix]/[slug]': 'LayoutSlug',
'/[prefix]/[slug]/[...suffix]': 'LayoutSlug', '/[prefix]/[slug]/[...suffix]': 'LayoutSlug',
'/auth/result': 'LayoutAuth',
'/sign-in/[[...index]]': 'LayoutSignIn', '/sign-in/[[...index]]': 'LayoutSignIn',
'/sign-up/[[...index]]': 'LayoutSignUp' '/sign-up/[[...index]]': 'LayoutSignUp'
}, },

View File

@@ -1,5 +1,10 @@
{ {
"compilerOptions": { "compilerOptions": {
"target": "es6",
"module": "commonjs",
"jsx": "react",
"allowJs": true,
"checkJs": true,
"baseUrl": ".", "baseUrl": ".",
"paths": { "paths": {
"@/*": ["./*"], "@/*": ["./*"],

View File

@@ -0,0 +1,119 @@
const axios = require('axios')
// 发送 Notion API 请求
async function postNotion(properties, databaseId, listContentMain, token) {
const url = 'https://api.notion.com/v1/pages'
const children = listContentMain
.map(contentMain => {
if (contentMain.type === 'paragraph') {
return {
object: 'block',
type: 'paragraph',
paragraph: {
rich_text: [
{ type: 'text', text: { content: contentMain.content } }
]
}
}
} else if (['file', 'image'].includes(contentMain.type)) {
return {
object: 'block',
type: contentMain.type,
[contentMain.type]: {
type: 'external',
external: { url: contentMain.content }
}
}
}
return null
})
.filter(Boolean)
const payload = {
parent: { database_id: databaseId },
properties,
children
}
const headers = {
accept: 'application/json',
'Notion-Version': '2022-06-28',
'content-type': 'application/json',
Authorization: `Bearer ${token}`
}
try {
const response = await axios.post(url, payload, { headers })
return response
} catch (error) {
console.error('写入Notion异常', error)
throw new Error(`Error posting to Notion: ${error.message}`)
}
}
// 处理响应结果
function responseResult(response) {
if (response.status === 200) {
console.log('成功...')
console.log(response.data)
} else {
console.log('失败...')
console.log(response.data)
}
}
// 准备属性字段
function notionProperty(id, avatar, name, mail, lastLoginTime, token) {
return {
id: {
rich_text: [
{
type: 'text',
text: {
content: id,
link: null
}
}
]
},
avatar: {
files: [
{
name: 'Project Alpha blueprint',
external: {
url: avatar
}
}
]
},
name: {
title: [
{
text: {
content: name
}
}
]
},
mail: {
email: mail
},
last_login_time: {
date: {
start: lastLoginTime
}
},
token: {
rich_text: [
{
type: 'text',
text: {
content: token,
link: null
}
}
]
}
}
}

View File

@@ -1,40 +1,57 @@
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server' import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
/** /**
* clerk 身份验证中间件 * Clerk 身份验证中间件
*/ */
export const config = { export const config = {
// 这里设置白名单,防止静态资源被拦截 // 这里设置白名单,防止静态资源被拦截
matcher: ['/((?!.*\\..*|_next|/sign-in).*)', '/', '/(api|trpc)(.*)'] matcher: ['/((?!.*\\..*|_next|/sign-in|/auth).*)', '/', '/(api|trpc)(.*)']
} }
// 被保护的路由 // 限制登录访问的路由
const isTenantRoute = createRouteMatcher([ const isTenantRoute = createRouteMatcher([
'/user/organization-selector(.*)', '/user/organization-selector(.*)',
'/user/orgid/(.*)' '/user/orgid/(.*)'
]) ])
// 限制权限的路由 // 限制权限访问的路由
const isTenantAdminRoute = createRouteMatcher([ const isTenantAdminRoute = createRouteMatcher([
'/admin/(.*)/memberships', '/admin/(.*)/memberships',
'/admin/(.*)/domain' '/admin/(.*)/domain'
]) ])
// 路由登录及权限检查 /**
export default clerkMiddleware( * 没有配置权限相关功能的返回
(auth, req) => { * @param req
// Restrict admin routes to users with specific permissions * @param ev
if (isTenantAdminRoute(req)) { * @returns
auth().protect(has => { */
return ( const noAuthMiddleware = async (req, ev) => {
has({ permission: 'org:sys_memberships:manage' }) || // 如果没有配置 Clerk 相关环境变量,返回一个默认响应或者继续处理请求
has({ permission: 'org:sys_domains_manage' }) return NextResponse.next()
) }
})
} /**
// Restrict organization routes to signed in users * 鉴权中间件
if (isTenantRoute(req)) auth().protect() */
} const authMiddleware = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
// { debug: process.env.npm_lifecycle_event === 'dev' } // 开发调试模式打印日志 ? clerkMiddleware(
) (auth, req) => {
// 限制管理员路由访问权限
if (isTenantAdminRoute(req)) {
auth().protect(has => {
return (
has({ permission: 'org:sys_memberships:manage' }) ||
has({ permission: 'org:sys_domains_manage' })
)
})
}
// 限制组织路由访问权限
if (isTenantRoute(req)) auth().protect()
}
// { debug: process.env.npm_lifecycle_event === 'dev' } // 开发调试模式打印日志
)
: noAuthMiddleware
export default authMiddleware

View File

@@ -27,6 +27,7 @@
"@next/bundle-analyzer": "^12.1.1", "@next/bundle-analyzer": "^12.1.1",
"@vercel/analytics": "^1.0.0", "@vercel/analytics": "^1.0.0",
"algoliasearch": "^4.18.0", "algoliasearch": "^4.18.0",
"axios": "^1.7.2",
"feed": "^4.2.2", "feed": "^4.2.2",
"js-md5": "^0.7.3", "js-md5": "^0.7.3",
"lodash.throttle": "^4.1.1", "lodash.throttle": "^4.1.1",
@@ -43,11 +44,13 @@
"react-tweet-embed": "~2.0.0" "react-tweet-embed": "~2.0.0"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^18.3.3", "@types/react": "18.3.3",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@waline/client": "^2.5.1", "@waline/client": "^2.5.1",
"autoprefixer": "^10.4.13", "autoprefixer": "^10.4.13",
"cross-env": "^7.0.3", "cross-env": "^7.0.3",
"eslint": "^7.26.0", "eslint": "^9.6.0",
"eslint-config-next": "^13.1.1", "eslint-config-next": "^13.1.1",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^16.0.2", "eslint-config-standard": "^16.0.2",
@@ -55,13 +58,13 @@
"eslint-plugin-node": "^11.1.0", "eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3", "eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^5.1.0", "eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.23.2", "eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-hooks": "^4.6.2",
"next-sitemap": "^1.6.203", "next-sitemap": "^1.6.203",
"postcss": "^8.4.31", "postcss": "^8.4.31",
"prettier": "3.2.5", "prettier": "^3.3.2",
"tailwindcss": "^3.3.2", "tailwindcss": "^3.3.2",
"typescript": "^5.4.5", "typescript": "5.5.3",
"webpack-bundle-analyzer": "^4.5.0" "webpack-bundle-analyzer": "^4.5.0"
}, },
"resolutions": { "resolutions": {

View File

@@ -0,0 +1,115 @@
// pages/api/auth.js
import axios from 'axios'
import type { NextApiRequest, NextApiResponse } from 'next'
/**
* Notion授权返回结果
*/
export interface NotionTokenResponseData {
access_token: string
token_type: string
bot_id: string
workspace_name: string
workspace_icon: string
workspace_id: string
owner: {
type: string
user: {
object: string
id: string
name: string
avatar_url: string
type: string
person: {
email: string
}
}
}
duplicated_template_id: string | null
request_id: string
}
export interface NotionTokenResponse {
status: number
statusText: string
data: NotionTokenResponseData
}
/**
* Notion授权回调
* @param req
* @param res
* @returns
*/
export default async function handler(
req: NextApiRequest,
res: NextApiResponse
) {
try {
const code = Array.isArray(req.query.code)
? req.query.code[0]
: req.query.code
if (!code) {
return res.status(400).json({ error: 'Invalid request, code is missing' })
}
const params = await fetchToken(code)
if (params?.status === 200) {
const redirectQuery = {
msg: '成功了' + JSON.stringify(params.data)
}
// 这里将用户数据写入到Notion数据库
res.redirect(302, `/auth/result?${new URLSearchParams(redirectQuery)}`)
} else {
const redirectQuery = { msg: params?.statusText || '请求异常' }
res.redirect(
302,
`/auth/result?${new URLSearchParams(redirectQuery).toString()}`
)
}
} catch (error) {
console.error(error)
res.status(500).json({ error: 'Internal Server Error' })
}
}
/**
* 获取token
* @param code
* @returns
*/
const fetchToken = async (code: string): Promise<NotionTokenResponse> => {
const clientId = process.env.OAUTH_CLIENT_ID
const clientSecret = process.env.OAUTH_CLIENT_SECRET
const redirectUri = process.env.OAUTH_REDIRECT_URI
const encoded = Buffer.from(`${clientId}:${clientSecret}`).toString('base64')
try {
const response = await axios.post<NotionTokenResponseData>(
'https://api.notion.com/v1/oauth/token',
{
grant_type: 'authorization_code',
code: code,
redirect_uri: redirectUri
},
{
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Authorization: `Basic ${encoded}`
}
}
)
console.log('OAuth身份信息', response.data)
return {
status: response.status,
statusText: response.statusText,
data: response.data
}
} catch (error) {
console.error('Error fetching token', error)
return null
}
}

33
pages/auth/result.js Normal file
View File

@@ -0,0 +1,33 @@
// pages/sitemap.xml.js
import { getGlobalData } from '@/lib/db/getSiteData'
import { useRouter } from 'next/router'
import Slug from '../[prefix]'
/**
/**
* 服务端接收参数处理
* @param {*} ctx
* @returns
*/
export const getServerSideProps = async ctx => {
const from = `auth`
const props = await getGlobalData({ from })
delete props.allPages
return {
props
}
}
/**
* 根据notion的slug访问页面
* 解析二级目录 /article/about
* @param {*} props
* @returns
*/
const UI = props => {
const router = useRouter()
return <Slug {...props} msg={router?.query?.msg} title={'授权结果'} />
}
export default UI

View File

@@ -276,17 +276,39 @@ const LayoutSignUp = props => {
) )
} }
export { /**
Layout404, * 授权页面
LayoutArchive, * @param {*} props
LayoutBase, * @returns
LayoutCategoryIndex, */
LayoutIndex, const LayoutAuth = props => {
LayoutPostList, const {msg,title} = props
LayoutSearch, return (
LayoutSignIn, <>
LayoutSignUp, <Banner title={title} description={msg} />
LayoutSlug, <div className='container grow'>
LayoutTagIndex, <div className='flex flex-wrap justify-center -mx-4'>
CONFIG as THEME_CONFIG <div className='w-full p-4'>
<div id='container-inner' className='mx-auto'>
{/* {slot} */}
</div>
</div>
</div>
</div>
</>
)
} }
export {
Layout404,
LayoutArchive, LayoutAuth, LayoutBase,
LayoutCategoryIndex,
LayoutIndex,
LayoutPostList,
LayoutSearch,
LayoutSignIn,
LayoutSignUp,
LayoutSlug, LayoutTagIndex,
CONFIG as THEME_CONFIG
}

View File

@@ -84,12 +84,9 @@ export const getLayoutByTheme = ({ router, theme }) => {
* @returns * @returns
*/ */
const getLayoutNameByPath = path => { const getLayoutNameByPath = path => {
if (LAYOUT_MAPPINGS[path]) { const layoutName = LAYOUT_MAPPINGS[path] || 'LayoutSlug'
return LAYOUT_MAPPINGS[path] // console.log('path-layout',path,layoutName)
} else { return layoutName
// 没有特殊处理的路径返回默认layout名称
return 'LayoutSlug'
}
} }
/** /**

View File

@@ -9,16 +9,13 @@
"@/lib/*": ["lib/*"], "@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"] "@/styles/*": ["styles/*"]
}, },
"lib": [ "lib": ["dom", "dom.iterable", "esnext"],
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true, "allowJs": true,
"skipLibCheck": true, "skipLibCheck": true,
"strict": false, "strict": false,
"noEmit": true, "noEmit": true,
"incremental": true, "incremental": true,
"target": "es6",
"module": "esnext", "module": "esnext",
"esModuleInterop": true, "esModuleInterop": true,
"moduleResolution": "node", "moduleResolution": "node",
@@ -28,12 +25,11 @@
}, },
"include": [ "include": [
"next-env.d.ts", "next-env.d.ts",
"**/*.json",
"**/*.js", "**/*.js",
"**/*.ts", "**/*.ts",
"**/*.tsx", "**/*.tsx",
"**/*.jsx" "**/*.jsx"
], ],
"exclude": [ "exclude": ["node_modules"]
"node_modules"
]
} }

1235
yarn.lock

File diff suppressed because it is too large Load Diff