Merge branch 'feature/user-auth-clerk' into release/4.7.3

This commit is contained in:
tangly1024.com
2024-09-23 17:38:58 +08:00
22 changed files with 1552 additions and 387 deletions

View File

@@ -4,15 +4,29 @@ module.exports = {
es2021: true,
node: true
},
extends: ['plugin:react/recommended', 'plugin:@next/next/recommended', 'standard', 'prettier'],
extends: [
'plugin:react/recommended',
'plugin:@next/next/recommended',
'standard',
'prettier',
'plugin:@typescript-eslint/recommended', // 添加 TypeScript 推荐规则
'plugin:@typescript-eslint/recommended-requiring-type-checking' // 添加需要类型检查的规则
],
parser: '@typescript-eslint/parser', // 使用 TypeScript 解析器
parserOptions: {
ecmaFeatures: {
jsx: true
},
ecmaVersion: 12,
sourceType: 'module'
sourceType: 'module',
project: './tsconfig.json' // 指定 tsconfig.json 的路径
},
plugins: ['react', 'react-hooks', 'prettier'],
plugins: [
'react',
'react-hooks',
'prettier',
'@typescript-eslint' // 添加 TypeScript 插件
],
settings: {
react: {
version: 'detect'
@@ -23,7 +37,9 @@ module.exports = {
'react/no-unknown-property': 'off', // <style jsx>
'react/prop-types': 'off',
'space-before-function-paren': 0,
'react-hooks/rules-of-hooks': 'error' // Checks rules of Hooks
'react-hooks/rules-of-hooks': 'error', // Checks rules of Hooks
'@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_' }], // 确保未使用的变量报错
'@typescript-eslint/explicit-function-return-type': 'off' // 关闭强制函数返回类型声明
},
globals: {
React: true

View File

@@ -125,8 +125,9 @@ const BLOG = {
'/[prefix]': 'LayoutSlug',
'/[prefix]/[slug]': 'LayoutSlug',
'/[prefix]/[slug]/[...suffix]': 'LayoutSlug',
'/signin': 'LayoutSignIn',
'/signup': 'LayoutSignUp'
'/auth/result': 'LayoutAuth',
'/sign-in/[[...index]]': 'LayoutSignIn',
'/sign-up/[[...index]]': 'LayoutSignUp'
},
CAN_COPY: process.env.NEXT_PUBLIC_CAN_COPY || true, // 是否允许复制页面内容 默认允许如果设置为false、则全栈禁止复制内容。

View File

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

View File

@@ -1,10 +1,10 @@
import { APPEARANCE, LANG, NOTION_PAGE_ID, THEME } from '@/blog.config'
import {
THEMES,
getThemeConfig,
initDarkMode,
saveDarkModeToLocalStorage
} from '@/themes/theme'
import { APPEARANCE, LANG, NOTION_PAGE_ID, THEME } from 'blog.config'
import { useRouter } from 'next/router'
import { createContext, useContext, useEffect, useState } from 'react'
import {

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
}
}
]
}
}
}

57
middleware.ts Normal file
View File

@@ -0,0 +1,57 @@
import { clerkMiddleware, createRouteMatcher } from '@clerk/nextjs/server'
import { NextResponse } from 'next/server'
/**
* Clerk 身份验证中间件
*/
export const config = {
// 这里设置白名单,防止静态资源被拦截
matcher: ['/((?!.*\\..*|_next|/sign-in|/auth).*)', '/', '/(api|trpc)(.*)']
}
// 限制登录访问的路由
const isTenantRoute = createRouteMatcher([
'/user/organization-selector(.*)',
'/user/orgid/(.*)'
])
// 限制权限访问的路由
const isTenantAdminRoute = createRouteMatcher([
'/admin/(.*)/memberships',
'/admin/(.*)/domain'
])
/**
* 没有配置权限相关功能的返回
* @param req
* @param ev
* @returns
*/
const noAuthMiddleware = async (req, ev) => {
// 如果没有配置 Clerk 相关环境变量,返回一个默认响应或者继续处理请求
return NextResponse.next()
}
/**
* 鉴权中间件
*/
const authMiddleware = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
? 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

5
next-env.d.ts vendored Normal file
View File

@@ -0,0 +1,5 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
// NOTE: This file should not be edited
// see https://nextjs.org/docs/basic-features/typescript for more information.

View File

@@ -81,6 +81,9 @@ function scanSubdirectories(directory) {
*/
const nextConfig = {
eslint: {
ignoreDuringBuilds: true
},
output: process.env.EXPORT ? 'export' : undefined,
staticPageGenerationTimeout: 120,
// 多语言, 在export时禁用

View File

@@ -22,10 +22,12 @@
"build-all-in-dev": "cross-env VERCEL_ENV=production next build"
},
"dependencies": {
"@clerk/nextjs": "^5.1.5",
"@headlessui/react": "^1.7.15",
"@next/bundle-analyzer": "^12.1.1",
"@vercel/analytics": "^1.0.0",
"algoliasearch": "^4.18.0",
"axios": "^1.7.2",
"feed": "^4.2.2",
"js-md5": "^0.7.3",
"lodash.throttle": "^4.1.1",
@@ -42,10 +44,13 @@
"react-tweet-embed": "~2.0.0"
},
"devDependencies": {
"@types/react": "18.3.3",
"@typescript-eslint/eslint-plugin": "^7.16.0",
"@typescript-eslint/parser": "^7.16.0",
"@waline/client": "^2.5.1",
"autoprefixer": "^10.4.13",
"cross-env": "^7.0.3",
"eslint": "^7.26.0",
"eslint": "^9.6.0",
"eslint-config-next": "^13.1.1",
"eslint-config-prettier": "^9.1.0",
"eslint-config-standard": "^16.0.2",
@@ -53,12 +58,13 @@
"eslint-plugin-node": "^11.1.0",
"eslint-plugin-prettier": "^5.1.3",
"eslint-plugin-promise": "^5.1.0",
"eslint-plugin-react": "^7.23.2",
"eslint-plugin-react-hooks": "^4.6.0",
"eslint-plugin-react": "^7.34.3",
"eslint-plugin-react-hooks": "^4.6.2",
"next-sitemap": "^1.6.203",
"postcss": "^8.4.31",
"prettier": "3.2.5",
"prettier": "^3.3.2",
"tailwindcss": "^3.3.2",
"typescript": "5.5.3",
"webpack-bundle-analyzer": "^4.5.0"
},
"resolutions": {

View File

@@ -31,7 +31,7 @@ const Slug = props => {
/**
* 验证文章密码
* @param {*} result
* @param {*} passInput
*/
const validPassword = passInput => {
if (!post) {

View File

@@ -17,6 +17,7 @@ import { getQueryParam } from '../lib/utils'
import BLOG from '@/blog.config'
import ExternalPlugins from '@/components/ExternalPlugins'
import GlobalHead from '@/components/GlobalHead'
import { ClerkProvider } from '@clerk/nextjs'
/**
* App挂载DOM 入口文件
@@ -46,7 +47,8 @@ const MyApp = ({ Component, pageProps }) => {
[queryParam]
)
return (
const enableClerk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
const content = (
<GlobalContextProvider {...pageProps}>
<GLayout {...pageProps}>
<GlobalHead {...pageProps} />
@@ -55,6 +57,7 @@ const MyApp = ({ Component, pageProps }) => {
<ExternalPlugins {...pageProps} />
</GlobalContextProvider>
)
return <>{enableClerk ? <ClerkProvider>{content}</ClerkProvider> : content}</>
}
export default MyApp

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
}
}

24
pages/api/user.ts Normal file
View File

@@ -0,0 +1,24 @@
import { getAuth } from '@clerk/nextjs/server'
import type { NextApiRequest, NextApiResponse } from 'next'
/**
* Clerk 身份测试
* @param req
* @param res
* @returns
*/
export default function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const { userId } = getAuth(req)
if (!userId) {
return res.status(401).json({ error: 'Unauthorized' })
}
// Retrieve data from your database
res.status(200).json({ userId })
} catch (error) {
console.error(error)
res.status(500).json({ error: 'Internal Server Error' })
}
}

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

@@ -0,0 +1,31 @@
// pages/sitemap.xml.js
import { getGlobalData } from '@/lib/db/getSiteData'
import { useRouter } from 'next/router'
import Slug from '../[prefix]'
/**
/**
* @returns
*/
export const getStaticProps = async () => {
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

@@ -1,6 +1,8 @@
// import BLOG from '@/blog.config'
import BLOG from '@/blog.config'
import { siteConfig } from '@/lib/config'
import { getGlobalData } from '@/lib/db/getSiteData'
// import { getGlobalData } from '@/lib/db/getSiteData'
import { getLayoutByTheme } from '@/themes/theme'
import { useRouter } from 'next/router'
@@ -37,4 +39,17 @@ export async function getStaticProps(req) {
}
}
/**
* catch-all route for clerk
* @returns
*/
export async function getStaticPaths() {
return {
paths: [
{ params: { index: [] } } // 使 /sign-in 路径可访问
],
fallback: true
}
}
export default SignIn

View File

@@ -37,4 +37,16 @@ export async function getStaticProps(req) {
}
}
/**
* catch-all route for clerk
* @returns
*/
export async function getStaticPaths() {
return {
paths: [
{ params: { index: [] } } // 使 /sign-in 路径可访问
],
fallback: true
}
}
export default SignUp

View File

@@ -1,6 +1,7 @@
/* eslint-disable no-unreachable */
import { siteConfig } from '@/lib/config'
import { useGlobal } from '@/lib/global'
import { SignedIn, SignedOut, UserButton } from '@clerk/nextjs'
import throttle from 'lodash.throttle'
import { useRouter } from 'next/router'
import { useCallback, useEffect, useState } from 'react'
@@ -11,12 +12,15 @@ import { MenuList } from './MenuList'
/**
* 顶部导航栏
*/
export const NavBar = props => {
export const Header = props => {
const router = useRouter()
const { isDarkMode } = useGlobal()
const [buttonTextColor, setColor] = useState(
router.route === '/' ? 'text-white' : ''
)
const enableClerk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
useEffect(() => {
if (isDarkMode || router.route === '/') {
setColor('text-white')
@@ -60,22 +64,27 @@ export const NavBar = props => {
<MenuList {...props} />
{/* 右侧功能 */}
<div className='flex items-center justify-end pr-16 lg:pr-0'>
<div className='flex items-center gap-4 justify-end pr-16 lg:pr-0'>
{/* 深色模式切换 */}
<DarkModeButton />
{/* 注册登录功能 */}
<div className='hidden sm:flex'>
<a
href={siteConfig('STARTER_NAV_BUTTON_1_URL')}
className={`loginBtn ${buttonTextColor} px-[22px] py-2 text-base font-medium hover:opacity-70`}>
{siteConfig('STARTER_NAV_BUTTON_1_TEXT')}
</a>
<a
href={siteConfig('STARTER_NAV_BUTTON_2_URL')}
className={`signUpBtn ${buttonTextColor} rounded-md bg-white bg-opacity-20 px-6 py-2 text-base font-medium duration-300 ease-in-out hover:bg-opacity-100 hover:text-dark`}>
{siteConfig('STARTER_NAV_BUTTON_2_TEXT')}
</a>
</div>
<SignedOut>
<div className='hidden sm:flex gap-4'>
<a
href={siteConfig('STARTER_NAV_BUTTON_1_URL')}
className={`loginBtn ${buttonTextColor} p-2 text-base font-medium hover:opacity-70`}>
{siteConfig('STARTER_NAV_BUTTON_1_TEXT')}
</a>
<a
href={siteConfig('STARTER_NAV_BUTTON_2_URL')}
className={`signUpBtn ${buttonTextColor} p-2 rounded-md bg-white bg-opacity-20 py-2 text-base font-medium duration-300 ease-in-out hover:bg-opacity-100 hover:text-dark`}>
{siteConfig('STARTER_NAV_BUTTON_2_TEXT')}
</a>
</div>
</SignedOut>
<SignedIn>
<UserButton />
</SignedIn>
</div>
</div>
</div>

View File

@@ -24,10 +24,10 @@ const CONFIG = {
// 顶部右侧导航暗流
STARTER_NAV_BUTTON_1_TEXT: 'Sign In',
STARTER_NAV_BUTTON_1_URL: '/signin',
STARTER_NAV_BUTTON_1_URL: '/sign-in',
STARTER_NAV_BUTTON_2_TEXT: 'Sign Up',
STARTER_NAV_BUTTON_2_URL: '/signup',
STARTER_NAV_BUTTON_2_URL: '/sign-up',
// 特性区块
STARTER_FEATURE_ENABLE: true, // 特性区块开关

View File

@@ -2,13 +2,6 @@
/* eslint-disable @next/next/no-img-element */
'use client'
/**
* 这是一个空白主题,方便您用作创建新主题时的模板,从而开发出您自己喜欢的主题
* 1. 禁用了代码质量检查功能提高了代码的宽容度您可以使用标准的html写法
* 2. 内容大部分是在此文件中写死notion数据从props参数中传进来
* 3. 您可在此网站找到更多喜欢的组件 https://www.tailwind-kit.com/
*/
import Loading from '@/components/Loading'
import NotionPage from '@/components/NotionPage'
import { siteConfig } from '@/lib/config'
@@ -23,8 +16,8 @@ import { Contact } from './components/Contact'
import { FAQ } from './components/FAQ'
import { Features } from './components/Features'
import { Footer } from './components/Footer'
import { Header } from './components/Header'
import { Hero } from './components/Hero'
import { NavBar } from './components/NavBar'
import { Pricing } from './components/Pricing'
import { Team } from './components/Team'
import { Testimonials } from './components/Testimonials'
@@ -32,6 +25,7 @@ import CONFIG from './config'
import { Style } from './style'
// import { MadeWithButton } from './components/MadeWithButton'
import { loadWowJS } from '@/lib/plugins/wow'
import { SignIn, SignUp } from '@clerk/nextjs'
import Link from 'next/link'
import { Banner } from './components/Banner'
import { CTA } from './components/CTA'
@@ -59,14 +53,17 @@ const LayoutBase = props => {
id='theme-starter'
className={`${siteConfig('FONT_STYLE')} min-h-screen flex flex-col dark:bg-[#212b36] scroll-smooth`}>
<Style />
<NavBar {...props} />
{/* 页头 */}
<Header {...props} />
{children}
{/* 页脚 */}
<Footer {...props} />
{/* 悬浮按钮 */}
<BackToTopButton />
{/* <MadeWithButton/> */}
</div>
)
@@ -102,7 +99,8 @@ const LayoutIndex = props => {
{siteConfig('STARTER_CONTACT_ENABLE') && <Contact />}
{/* 合作伙伴 */}
{siteConfig('STARTER_BRANDS_ENABLE') && <Brand />}
<CTA />
{/* 行动呼吁 */}
{siteConfig('STARTER_CTA_ENABLE') && <CTA />}
</>
)
}
@@ -221,6 +219,7 @@ const LayoutTagIndex = props => <></>
* @returns
*/
const LayoutSignIn = props => {
const enableClerk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
const title = siteConfig('STARTER_SIGNIN', '登录')
const description = siteConfig(
'STARTER_SIGNIN_DESCRITION',
@@ -230,7 +229,15 @@ const LayoutSignIn = props => {
<>
<div className='grow mt-20'>
<Banner title={title} description={description} />
<SignInForm />
{/* clerk预置表单 */}
{enableClerk && (
<div className='flex justify-center py-6'>
<SignIn />
</div>
)}
{/* 自定义登录表单 */}
{!enableClerk && <SignInForm />}
</div>
</>
)
@@ -242,6 +249,8 @@ const LayoutSignIn = props => {
* @returns
*/
const LayoutSignUp = props => {
const enableClerk = process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY
const title = siteConfig('STARTER_SIGNIN', '注册')
const description = siteConfig(
'STARTER_SIGNIN_DESCRITION',
@@ -251,7 +260,16 @@ const LayoutSignUp = props => {
<>
<div className='grow mt-20'>
<Banner title={title} description={description} />
<SignInForm />
{/* clerk预置表单 */}
{enableClerk && (
<div className='flex justify-center py-6'>
<SignUp />
</div>
)}
{/* 自定义登录表单 */}
{!enableClerk && <SignUpForm />}
</div>
</>
)

View File

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

35
tsconfig.json Normal file
View File

@@ -0,0 +1,35 @@
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["./*"],
"@/components/*": ["components/*"],
"@/theme/*": ["theme/*"],
"@/data/*": ["data/*"],
"@/lib/*": ["lib/*"],
"@/styles/*": ["styles/*"]
},
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"incremental": true,
"target": "es6",
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"include": [
"next-env.d.ts",
"**/*.json",
// "**/*.js",
"**/*.ts",
"**/*.tsx",
"**/*.jsx"
],
"exclude": ["node_modules"]
}

1374
yarn.lock

File diff suppressed because it is too large Load Diff