重构认证逻辑,优化登录和用户信息获取流程,新增全局缓存支持
This commit is contained in:
@@ -4,18 +4,10 @@
|
|||||||
|
|
||||||
使用 pure js 的包代替 canvas,加快编译速度
|
使用 pure js 的包代替 canvas,加快编译速度
|
||||||
|
|
||||||
重新设计验证逻辑,通过全局 cache 优化请求效率,使用服务端组件实现验证
|
|
||||||
|
|
||||||
提取后刷新提取页套餐可用余量
|
提取后刷新提取页套餐可用余量
|
||||||
|
|
||||||
提取时检查 IP 和实名状态
|
|
||||||
|
|
||||||
保存客户端信息时用 jwt 序列化
|
保存客户端信息时用 jwt 序列化
|
||||||
|
|
||||||
登录后刷新 profile
|
|
||||||
|
|
||||||
区分调用方式,提供 callByDevice callByUser call 三种调用方式
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
页面数据:
|
页面数据:
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
"date-fns": "^4.1.0",
|
"date-fns": "^4.1.0",
|
||||||
"lucide-react": "^0.479.0",
|
"lucide-react": "^0.479.0",
|
||||||
"motion": "^12.5.0",
|
"motion": "^12.5.0",
|
||||||
"next": "15.2.1",
|
"next": "15.2.4",
|
||||||
"next-themes": "^0.4.6",
|
"next-themes": "^0.4.6",
|
||||||
"qrcode": "^1.5.4",
|
"qrcode": "^1.5.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
@@ -38,7 +38,8 @@
|
|||||||
"sonner": "^2.0.1",
|
"sonner": "^2.0.1",
|
||||||
"tailwind-merge": "^3.0.2",
|
"tailwind-merge": "^3.0.2",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"zod": "^3.24.2"
|
"zod": "^3.24.2",
|
||||||
|
"zustand": "^5.0.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
|||||||
108
pnpm-lock.yaml
generated
108
pnpm-lock.yaml
generated
@@ -66,8 +66,8 @@ importers:
|
|||||||
specifier: ^12.5.0
|
specifier: ^12.5.0
|
||||||
version: 12.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 12.5.0(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
next:
|
next:
|
||||||
specifier: 15.2.1
|
specifier: 15.2.4
|
||||||
version: 15.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
next-themes:
|
next-themes:
|
||||||
specifier: ^0.4.6
|
specifier: ^0.4.6
|
||||||
version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
version: 0.4.6(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||||
@@ -98,6 +98,9 @@ importers:
|
|||||||
zod:
|
zod:
|
||||||
specifier: ^3.24.2
|
specifier: ^3.24.2
|
||||||
version: 3.24.2
|
version: 3.24.2
|
||||||
|
zustand:
|
||||||
|
specifier: ^5.0.3
|
||||||
|
version: 5.0.3(@types/react@19.0.10)(react@19.0.0)
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@eslint/eslintrc':
|
'@eslint/eslintrc':
|
||||||
specifier: ^3
|
specifier: ^3
|
||||||
@@ -339,60 +342,60 @@ packages:
|
|||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@next/env@15.2.1':
|
'@next/env@15.2.4':
|
||||||
resolution: {integrity: sha512-JmY0qvnPuS2NCWOz2bbby3Pe0VzdAQ7XpEB6uLIHmtXNfAsAO0KLQLkuAoc42Bxbo3/jMC3dcn9cdf+piCcG2Q==}
|
resolution: {integrity: sha512-+SFtMgoiYP3WoSswuNmxJOCwi06TdWE733D+WPjpXIe4LXGULwEaofiiAy6kbS0+XjM5xF5n3lKuBwN2SnqD9g==}
|
||||||
|
|
||||||
'@next/eslint-plugin-next@15.2.1':
|
'@next/eslint-plugin-next@15.2.1':
|
||||||
resolution: {integrity: sha512-6ppeToFd02z38SllzWxayLxjjNfzvc7Wm07gQOKSLjyASvKcXjNStZrLXMHuaWkhjqxe+cnhb2uzfWXm1VEj/Q==}
|
resolution: {integrity: sha512-6ppeToFd02z38SllzWxayLxjjNfzvc7Wm07gQOKSLjyASvKcXjNStZrLXMHuaWkhjqxe+cnhb2uzfWXm1VEj/Q==}
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@15.2.1':
|
'@next/swc-darwin-arm64@15.2.4':
|
||||||
resolution: {integrity: sha512-aWXT+5KEREoy3K5AKtiKwioeblmOvFFjd+F3dVleLvvLiQ/mD//jOOuUcx5hzcO9ISSw4lrqtUPntTpK32uXXQ==}
|
resolution: {integrity: sha512-1AnMfs655ipJEDC/FHkSr0r3lXBgpqKo4K1kiwfUf3iE68rDFXZ1TtHdMvf7D0hMItgDZ7Vuq3JgNMbt/+3bYw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/swc-darwin-x64@15.2.1':
|
'@next/swc-darwin-x64@15.2.4':
|
||||||
resolution: {integrity: sha512-E/w8ervu4fcG5SkLhvn1NE/2POuDCDEy5gFbfhmnYXkyONZR68qbUlJlZwuN82o7BrBVAw+tkR8nTIjGiMW1jQ==}
|
resolution: {integrity: sha512-3qK2zb5EwCwxnO2HeO+TRqCubeI/NgCe+kL5dTJlPldV/uwCnUgC7VbEzgmxbfrkbjehL4H9BPztWOEtsoMwew==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [darwin]
|
os: [darwin]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-gnu@15.2.1':
|
'@next/swc-linux-arm64-gnu@15.2.4':
|
||||||
resolution: {integrity: sha512-gXDX5lIboebbjhiMT6kFgu4svQyjoSed6dHyjx5uZsjlvTwOAnZpn13w9XDaIMFFHw7K8CpBK7HfDKw0VZvUXQ==}
|
resolution: {integrity: sha512-HFN6GKUcrTWvem8AZN7tT95zPb0GUGv9v0d0iyuTb303vbXkkbHDp/DxufB04jNVD+IN9yHy7y/6Mqq0h0YVaQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@15.2.1':
|
'@next/swc-linux-arm64-musl@15.2.4':
|
||||||
resolution: {integrity: sha512-3v0pF/adKZkBWfUffmB/ROa+QcNTrnmYG4/SS+r52HPwAK479XcWoES2I+7F7lcbqc7mTeVXrIvb4h6rR/iDKg==}
|
resolution: {integrity: sha512-Oioa0SORWLwi35/kVB8aCk5Uq+5/ZIumMK1kJV+jSdazFm2NzPDztsefzdmzzpx5oGCJ6FkUC7vkaUseNTStNA==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@15.2.1':
|
'@next/swc-linux-x64-gnu@15.2.4':
|
||||||
resolution: {integrity: sha512-RbsVq2iB6KFJRZ2cHrU67jLVLKeuOIhnQB05ygu5fCNgg8oTewxweJE8XlLV+Ii6Y6u4EHwETdUiRNXIAfpBww==}
|
resolution: {integrity: sha512-yb5WTRaHdkgOqFOZiu6rHV1fAEK0flVpaIN2HB6kxHVSy/dIajWbThS7qON3W9/SNOH2JWkVCyulgGYekMePuw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [glibc]
|
libc: [glibc]
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@15.2.1':
|
'@next/swc-linux-x64-musl@15.2.4':
|
||||||
resolution: {integrity: sha512-QHsMLAyAIu6/fWjHmkN/F78EFPKmhQlyX5C8pRIS2RwVA7z+t9cTb0IaYWC3EHLOTjsU7MNQW+n2xGXr11QPpg==}
|
resolution: {integrity: sha512-Dcdv/ix6srhkM25fgXiyOieFUkz+fOYkHlydWCtB0xMST6X9XYI3yPDKBZt1xuhOytONsIFJFB08xXYsxUwJLw==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [linux]
|
os: [linux]
|
||||||
libc: [musl]
|
libc: [musl]
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@15.2.1':
|
'@next/swc-win32-arm64-msvc@15.2.4':
|
||||||
resolution: {integrity: sha512-Gk42XZXo1cE89i3hPLa/9KZ8OuupTjkDmhLaMKFohjf9brOeZVEa3BQy1J9s9TWUqPhgAEbwv6B2+ciGfe54Vw==}
|
resolution: {integrity: sha512-dW0i7eukvDxtIhCYkMrZNQfNicPDExt2jPb9AZPpL7cfyUo7QSNl1DjsHjmmKp6qNAqUESyT8YFl/Aw91cNJJg==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [arm64]
|
cpu: [arm64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
|
|
||||||
'@next/swc-win32-x64-msvc@15.2.1':
|
'@next/swc-win32-x64-msvc@15.2.4':
|
||||||
resolution: {integrity: sha512-YjqXCl8QGhVlMR8uBftWk0iTmvtntr41PhG1kvzGp0sUP/5ehTM+cwx25hKE54J0CRnHYjSGjSH3gkHEaHIN9g==}
|
resolution: {integrity: sha512-SbnWkJmkS7Xl3kre8SdMF6F/XDh1DTFEhp0jRTj/uB8iPKoU2bb2NDfcu+iifv1+mxQEd1g2vvSxcZbXSKyWiQ==}
|
||||||
engines: {node: '>= 10'}
|
engines: {node: '>= 10'}
|
||||||
cpu: [x64]
|
cpu: [x64]
|
||||||
os: [win32]
|
os: [win32]
|
||||||
@@ -2136,8 +2139,8 @@ packages:
|
|||||||
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc
|
||||||
|
|
||||||
next@15.2.1:
|
next@15.2.4:
|
||||||
resolution: {integrity: sha512-zxbsdQv3OqWXybK5tMkPCBKyhIz63RstJ+NvlfkaLMc/m5MwXgz2e92k+hSKcyBpyADhMk2C31RIiaDjUZae7g==}
|
resolution: {integrity: sha512-VwL+LAaPSxEkd3lU2xWbgEOtrM8oedmyhBqaVNmgKB+GvZlCy9rgaEc+y2on0wv+l0oSFqLtYD6dcC1eAedUaQ==}
|
||||||
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
engines: {node: ^18.18.0 || ^19.8.0 || >= 20.0.0}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@@ -2715,6 +2718,24 @@ packages:
|
|||||||
zod@3.24.2:
|
zod@3.24.2:
|
||||||
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
|
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
|
||||||
|
|
||||||
|
zustand@5.0.3:
|
||||||
|
resolution: {integrity: sha512-14fwWQtU3pH4dE0dOpdMiWjddcH+QzKIgk1cl8epwSE7yag43k/AD/m4L6+K7DytAOr9gGBe3/EXj9g7cdostg==}
|
||||||
|
engines: {node: '>=12.20.0'}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '>=18.0.0'
|
||||||
|
immer: '>=9.0.6'
|
||||||
|
react: '>=18.0.0'
|
||||||
|
use-sync-external-store: '>=1.2.0'
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
immer:
|
||||||
|
optional: true
|
||||||
|
react:
|
||||||
|
optional: true
|
||||||
|
use-sync-external-store:
|
||||||
|
optional: true
|
||||||
|
|
||||||
snapshots:
|
snapshots:
|
||||||
|
|
||||||
'@alloc/quick-lru@5.2.0': {}
|
'@alloc/quick-lru@5.2.0': {}
|
||||||
@@ -2876,34 +2897,34 @@ snapshots:
|
|||||||
'@img/sharp-win32-x64@0.33.5':
|
'@img/sharp-win32-x64@0.33.5':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/env@15.2.1': {}
|
'@next/env@15.2.4': {}
|
||||||
|
|
||||||
'@next/eslint-plugin-next@15.2.1':
|
'@next/eslint-plugin-next@15.2.1':
|
||||||
dependencies:
|
dependencies:
|
||||||
fast-glob: 3.3.1
|
fast-glob: 3.3.1
|
||||||
|
|
||||||
'@next/swc-darwin-arm64@15.2.1':
|
'@next/swc-darwin-arm64@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-darwin-x64@15.2.1':
|
'@next/swc-darwin-x64@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-arm64-gnu@15.2.1':
|
'@next/swc-linux-arm64-gnu@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-arm64-musl@15.2.1':
|
'@next/swc-linux-arm64-musl@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-x64-gnu@15.2.1':
|
'@next/swc-linux-x64-gnu@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-linux-x64-musl@15.2.1':
|
'@next/swc-linux-x64-musl@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-arm64-msvc@15.2.1':
|
'@next/swc-win32-arm64-msvc@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@next/swc-win32-x64-msvc@15.2.1':
|
'@next/swc-win32-x64-msvc@15.2.4':
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
@@ -4735,9 +4756,9 @@ snapshots:
|
|||||||
react: 19.0.0
|
react: 19.0.0
|
||||||
react-dom: 19.0.0(react@19.0.0)
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
|
|
||||||
next@15.2.1(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
next@15.2.4(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@next/env': 15.2.1
|
'@next/env': 15.2.4
|
||||||
'@swc/counter': 0.1.3
|
'@swc/counter': 0.1.3
|
||||||
'@swc/helpers': 0.5.15
|
'@swc/helpers': 0.5.15
|
||||||
busboy: 1.6.0
|
busboy: 1.6.0
|
||||||
@@ -4747,14 +4768,14 @@ snapshots:
|
|||||||
react-dom: 19.0.0(react@19.0.0)
|
react-dom: 19.0.0(react@19.0.0)
|
||||||
styled-jsx: 5.1.6(react@19.0.0)
|
styled-jsx: 5.1.6(react@19.0.0)
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@next/swc-darwin-arm64': 15.2.1
|
'@next/swc-darwin-arm64': 15.2.4
|
||||||
'@next/swc-darwin-x64': 15.2.1
|
'@next/swc-darwin-x64': 15.2.4
|
||||||
'@next/swc-linux-arm64-gnu': 15.2.1
|
'@next/swc-linux-arm64-gnu': 15.2.4
|
||||||
'@next/swc-linux-arm64-musl': 15.2.1
|
'@next/swc-linux-arm64-musl': 15.2.4
|
||||||
'@next/swc-linux-x64-gnu': 15.2.1
|
'@next/swc-linux-x64-gnu': 15.2.4
|
||||||
'@next/swc-linux-x64-musl': 15.2.1
|
'@next/swc-linux-x64-musl': 15.2.4
|
||||||
'@next/swc-win32-arm64-msvc': 15.2.1
|
'@next/swc-win32-arm64-msvc': 15.2.4
|
||||||
'@next/swc-win32-x64-msvc': 15.2.1
|
'@next/swc-win32-x64-msvc': 15.2.4
|
||||||
sharp: 0.33.5
|
sharp: 0.33.5
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@babel/core'
|
- '@babel/core'
|
||||||
@@ -5435,3 +5456,8 @@ snapshots:
|
|||||||
yocto-queue@0.1.0: {}
|
yocto-queue@0.1.0: {}
|
||||||
|
|
||||||
zod@3.24.2: {}
|
zod@3.24.2: {}
|
||||||
|
|
||||||
|
zustand@5.0.3(@types/react@19.0.10)(react@19.0.0):
|
||||||
|
optionalDependencies:
|
||||||
|
'@types/react': 19.0.10
|
||||||
|
react: 19.0.0
|
||||||
|
|||||||
@@ -1,62 +1,57 @@
|
|||||||
'use server'
|
'use server'
|
||||||
import {cookies} from 'next/headers'
|
import {cookies} from 'next/headers'
|
||||||
import {ApiResponse} from '@/lib/api'
|
import {ApiResponse, UnauthorizedError} from '@/lib/api'
|
||||||
import {AuthContext} from '@/lib/auth'
|
import {AuthContext} from '@/lib/auth'
|
||||||
import {User} from '@/lib/models'
|
import {User} from '@/lib/models'
|
||||||
import {callByDevice, callByUser, getUserToken} from '@/actions/base'
|
import {callByDevice, callByUser, callPublic, getUserToken} from '@/actions/base'
|
||||||
import {redirect} from 'next/navigation'
|
import {redirect} from 'next/navigation'
|
||||||
|
import {cache} from 'react'
|
||||||
|
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
username: string
|
username: string
|
||||||
password: string
|
password: string
|
||||||
remember?: boolean
|
remember: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
type LoginResp = {
|
type LoginResp = {
|
||||||
access_token: string
|
access_token: string
|
||||||
refresh_token: string
|
refresh_token: string
|
||||||
expires: number
|
expires_in: number
|
||||||
auth: AuthContext
|
token_type: string
|
||||||
profile: User
|
scope?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function login(props: LoginParams): Promise<ApiResponse> {
|
export async function login(props: LoginParams): Promise<ApiResponse> {
|
||||||
// 尝试登录
|
// 尝试登录
|
||||||
const result = await callByDevice<LoginResp>('/api/auth/login/sms', {
|
const result = await callByDevice<LoginResp>('/api/auth/token', {
|
||||||
username: props.username,
|
...props,
|
||||||
password: props.password,
|
grant_type: 'password',
|
||||||
remember: props.remember ?? false,
|
login_type: 'phone_code',
|
||||||
})
|
})
|
||||||
if (!result.success) {
|
if (!result.success) {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
const data = result.data
|
|
||||||
|
|
||||||
// 保存到 cookies
|
// 保存到 cookies
|
||||||
const current = Math.floor(Date.now() / 1000)
|
const data = result.data
|
||||||
const future = data.expires - current
|
|
||||||
|
|
||||||
const cookieStore = await cookies()
|
const cookieStore = await cookies()
|
||||||
cookieStore.set('auth_token', data.access_token, {
|
cookieStore.set('auth_token', data.access_token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
maxAge: Math.max(future, 0),
|
maxAge: Math.max(data.expires_in, 0),
|
||||||
})
|
})
|
||||||
cookieStore.set('auth_refresh', data.refresh_token, {
|
cookieStore.set('auth_refresh', data.refresh_token, {
|
||||||
httpOnly: true,
|
httpOnly: true,
|
||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
maxAge: 7 * 24 * 3600,
|
|
||||||
})
|
|
||||||
cookieStore.set('auth_info', JSON.stringify(data.auth), {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: 7 * 24 * 3600,
|
|
||||||
})
|
|
||||||
cookieStore.set('auth_profile', JSON.stringify(data.profile), {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: 7 * 24 * 3600,
|
|
||||||
})
|
})
|
||||||
|
// cookieStore.set('auth_info', JSON.stringify(data.auth), {
|
||||||
|
// httpOnly: true,
|
||||||
|
// sameSite: 'strict',
|
||||||
|
// })
|
||||||
|
// cookieStore.set('auth_profile', JSON.stringify(data.profile), {
|
||||||
|
// httpOnly: true,
|
||||||
|
// sameSite: 'strict',
|
||||||
|
// })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
@@ -88,52 +83,27 @@ export async function logout() {
|
|||||||
sameSite: 'strict',
|
sameSite: 'strict',
|
||||||
maxAge: -1,
|
maxAge: -1,
|
||||||
})
|
})
|
||||||
cookieStore.set('auth_info', '', {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: -1,
|
|
||||||
})
|
|
||||||
cookieStore.set('auth_profile', '', {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: -1,
|
|
||||||
})
|
|
||||||
|
|
||||||
return redirect('/')
|
return {
|
||||||
|
success: true,
|
||||||
|
data: undefined,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getProfile(refresh: boolean = false) {
|
export async function getProfile() {
|
||||||
const cookie = await cookies()
|
|
||||||
|
|
||||||
// 获取缓存的用户信息
|
|
||||||
if (!refresh) {
|
|
||||||
const profile = cookie.get('auth_profile')?.value
|
|
||||||
if (profile) {
|
|
||||||
return JSON.parse(profile) as User
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取缓存的 token
|
|
||||||
let token: string
|
|
||||||
try {
|
try {
|
||||||
token = await getUserToken()
|
const token = await getUserToken()
|
||||||
|
const result = await callPublic<User>('/api/user/get/token', {token})
|
||||||
|
|
||||||
|
if (!result.success) {
|
||||||
|
throw new Error('获取用户信息失败')
|
||||||
|
}
|
||||||
|
return result.data
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
return null
|
if (e === UnauthorizedError) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果没有缓存,则请求用户信息
|
|
||||||
const result = await callByUser<User>('/api/user/get/token', {token})
|
|
||||||
if (!result.success) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存用户信息到cookie
|
|
||||||
cookie.set('auth_profile', JSON.stringify(result.data), {
|
|
||||||
httpOnly: true,
|
|
||||||
sameSite: 'strict',
|
|
||||||
maxAge: 7 * 24 * 3600,
|
|
||||||
})
|
|
||||||
|
|
||||||
return result.data
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,106 +2,52 @@
|
|||||||
import {API_BASE_URL, CLIENT_ID, CLIENT_SECRET, ApiResponse, UnauthorizedError} from '@/lib/api'
|
import {API_BASE_URL, CLIENT_ID, CLIENT_SECRET, ApiResponse, UnauthorizedError} from '@/lib/api'
|
||||||
import {cookies, headers} from 'next/headers'
|
import {cookies, headers} from 'next/headers'
|
||||||
import {redirect} from 'next/navigation'
|
import {redirect} from 'next/navigation'
|
||||||
|
import {cache} from 'react'
|
||||||
// OAuth令牌缓存
|
|
||||||
interface TokenCache {
|
|
||||||
token: string
|
|
||||||
expires: number // 过期时间戳
|
|
||||||
}
|
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// region device token
|
// region device token
|
||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
let tokenCache: TokenCache | null = null
|
async function callByDevice<R = undefined>(
|
||||||
|
endpoint: string,
|
||||||
// 获取OAuth2访问令牌
|
data: unknown,
|
||||||
async function getDeviceToken(forceRefresh = false): Promise<string> {
|
): Promise<ApiResponse<R>> {
|
||||||
try {
|
return _callByDevice(endpoint, data ? JSON.stringify(data) : undefined)
|
||||||
// 检查缓存的令牌是否可用
|
|
||||||
if (!forceRefresh && tokenCache && tokenCache.expires > Date.now()) {
|
|
||||||
return tokenCache.token
|
|
||||||
}
|
|
||||||
|
|
||||||
const addr = `${API_BASE_URL}/api/auth/token`
|
|
||||||
const body = {
|
|
||||||
client_id: CLIENT_ID,
|
|
||||||
client_secret: CLIENT_SECRET,
|
|
||||||
grant_type: 'client_credentials',
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(addr, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
},
|
|
||||||
body: JSON.stringify(body),
|
|
||||||
})
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`OAuth token request failed: ${response.status} ${await response.text()}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await response.json()
|
|
||||||
|
|
||||||
// 缓存令牌和过期时间
|
|
||||||
// 通常后端会返回expires_in(秒为单位)
|
|
||||||
tokenCache = {
|
|
||||||
token: data.access_token,
|
|
||||||
expires: Date.now() + data.expires_in * 1000,
|
|
||||||
}
|
|
||||||
|
|
||||||
return tokenCache.token
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error('Failed to get access token:', error)
|
|
||||||
throw new Error('认证服务暂时不可用')
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 通用的API调用函数
|
// 通用的API调用函数
|
||||||
async function callByDevice<R = undefined>(endpoint: string, data: unknown): Promise<ApiResponse<R>> {
|
const _callByDevice = cache(async <R = undefined>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: string,
|
||||||
|
): Promise<ApiResponse<R>> => {
|
||||||
try {
|
try {
|
||||||
// 发送请求
|
// 获取设备令牌
|
||||||
let accessToken = getDeviceToken()
|
if (!CLIENT_ID || !CLIENT_SECRET) {
|
||||||
const requestOptions = {
|
throw new Error('缺少CLIENT_ID或CLIENT_SECRET环境变量')
|
||||||
|
}
|
||||||
|
const token = Buffer.from(`${CLIENT_ID}:${CLIENT_SECRET}`).toString('base64url')
|
||||||
|
|
||||||
|
// 构造请求
|
||||||
|
const url = `${API_BASE_URL}${endpoint}`
|
||||||
|
const request = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${await accessToken}`,
|
'Authorization': `Basic ${token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify(data),
|
body: data,
|
||||||
}
|
|
||||||
let response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
|
|
||||||
|
|
||||||
// 如果返回401未授权,尝试刷新令牌并重试一次
|
|
||||||
if (response.status === 401) {
|
|
||||||
accessToken = getDeviceToken(true) // 强制刷新令牌
|
|
||||||
|
|
||||||
// 使用新令牌重试请求
|
|
||||||
requestOptions.headers['Authorization'] = `Bearer ${await accessToken}`
|
|
||||||
response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查响应状态
|
// 发送请求
|
||||||
if (!response.ok) {
|
const response = await fetch(`${API_BASE_URL}${endpoint}`, request)
|
||||||
console.log('响应不成功', `status=${response.status}`, await response.text())
|
|
||||||
return {
|
|
||||||
success: false,
|
|
||||||
status: response.status,
|
|
||||||
message: '请求失败',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查响应状态
|
// 检查响应状态
|
||||||
return handleResponse(response)
|
return handleResponse(response)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('API call failed:', e)
|
|
||||||
throw new Error('服务调用失败', {cause: e})
|
throw new Error('服务调用失败', {cause: e})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@@ -172,26 +118,33 @@ async function callByUser<R = undefined>(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
data?: unknown,
|
data?: unknown,
|
||||||
): Promise<ApiResponse<R>> {
|
): Promise<ApiResponse<R>> {
|
||||||
|
return _callByUser(endpoint, data ? JSON.stringify(data) : undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _callByUser = cache(async <R = undefined>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: string,
|
||||||
|
): Promise<ApiResponse<R>> => {
|
||||||
|
const header = await headers()
|
||||||
try {
|
try {
|
||||||
let token = await getUserToken()
|
let token = await getUserToken()
|
||||||
const header = await headers()
|
|
||||||
|
// 构造请求
|
||||||
// 获取客户端 IP
|
|
||||||
const clientIp = header.get('x-forwarded-for')
|
|
||||||
|
|
||||||
// 发送请求
|
|
||||||
const request = {
|
const request = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'Authorization': `Bearer ${token}`,
|
'Authorization': `Bearer ${token}`,
|
||||||
} as Record<string, string>,
|
} as Record<string, string>,
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
body: data,
|
||||||
}
|
|
||||||
if (clientIp) {
|
|
||||||
request.headers['X-Forwarded-For'] = clientIp
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userIp = header.get('x-forwarded-for')
|
||||||
|
if (userIp) {
|
||||||
|
request.headers['X-Forwarded-For'] = userIp
|
||||||
|
}
|
||||||
|
|
||||||
|
// 发送请求
|
||||||
let response = await fetch(`${API_BASE_URL}${endpoint}`, request)
|
let response = await fetch(`${API_BASE_URL}${endpoint}`, request)
|
||||||
if (response.status === 401) {
|
if (response.status === 401) {
|
||||||
token = await getUserToken(true)
|
token = await getUserToken(true)
|
||||||
@@ -199,45 +152,25 @@ async function callByUser<R = undefined>(
|
|||||||
response = await fetch(`${API_BASE_URL}${endpoint}`, request)
|
response = await fetch(`${API_BASE_URL}${endpoint}`, request)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查响应状态
|
if (response.status === 401) {
|
||||||
if (!response.ok) {
|
throw UnauthorizedError
|
||||||
const body = await response.text()
|
|
||||||
console.log('响应不成功', `status=${response.status}`, body)
|
|
||||||
|
|
||||||
if (response.status === 401) {
|
|
||||||
return redirect('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status >= 400 && response.status < 500) {
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
success: false,
|
|
||||||
message: body,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (response.status >= 500) {
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
success: false,
|
|
||||||
message: '服务器错误',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
success: false,
|
|
||||||
message: `请求失败,status = ${response.status}`,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return handleResponse(response)
|
return handleResponse(response)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('API call with user token failed:', e)
|
if (e === UnauthorizedError) {
|
||||||
|
const referer = header.get('referer')
|
||||||
|
let redirectUrl = '/login'
|
||||||
|
if (referer) {
|
||||||
|
const url = new URL(referer)
|
||||||
|
redirectUrl = `/login?redirect=${encodeURIComponent(url.pathname)}`
|
||||||
|
}
|
||||||
|
return redirect(redirectUrl)
|
||||||
|
}
|
||||||
throw new Error('服务调用失败', {cause: e})
|
throw new Error('服务调用失败', {cause: e})
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
@@ -250,43 +183,35 @@ async function callPublic<R = undefined>(
|
|||||||
endpoint: string,
|
endpoint: string,
|
||||||
data?: unknown,
|
data?: unknown,
|
||||||
): Promise<ApiResponse<R>> {
|
): Promise<ApiResponse<R>> {
|
||||||
|
return _callPublic(endpoint, data ? JSON.stringify(data) : undefined)
|
||||||
|
}
|
||||||
|
|
||||||
|
const _callPublic = cache(async <R = undefined>(
|
||||||
|
endpoint: string,
|
||||||
|
data?: string,
|
||||||
|
): Promise<ApiResponse<R>> => {
|
||||||
try {
|
try {
|
||||||
// 发送请求
|
const url = `${API_BASE_URL}${endpoint}`
|
||||||
const requestOptions: RequestInit = {
|
const request: RequestInit = {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
},
|
},
|
||||||
body: data ? JSON.stringify(data) : undefined,
|
body: data,
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(`${API_BASE_URL}${endpoint}`, requestOptions)
|
|
||||||
|
|
||||||
// 检查响应状态
|
|
||||||
if (!response.ok) {
|
|
||||||
console.log('公共接口响应不成功', `status=${response.status}`, await response.text())
|
|
||||||
|
|
||||||
return {
|
|
||||||
status: response.status,
|
|
||||||
success: false,
|
|
||||||
message: response.status >= 500 ? '服务器错误' : '请求失败',
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const response = await fetch(url, request)
|
||||||
return handleResponse(response)
|
return handleResponse(response)
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.error('Public API call failed:', e)
|
throw new Error('服务调用失败', {cause: e})
|
||||||
throw new Error('服务调用失败', { cause: e })
|
|
||||||
}
|
}
|
||||||
}
|
})
|
||||||
|
|
||||||
// endregion
|
// endregion
|
||||||
|
|
||||||
// 统一响应解析
|
// 统一响应解析
|
||||||
async function handleResponse<R = undefined>(response: Response): Promise<ApiResponse<R>> {
|
async function handleResponse<R = undefined>(response: Response): Promise<ApiResponse<R>> {
|
||||||
|
|
||||||
// 解析响应数据
|
|
||||||
const type = response.headers.get('Content-Type') ?? 'text/plain'
|
const type = response.headers.get('Content-Type') ?? 'text/plain'
|
||||||
if (type.indexOf('application/json') !== -1) {
|
if (type.indexOf('application/json') !== -1) {
|
||||||
const json = await response.json()
|
const json = await response.json()
|
||||||
@@ -313,9 +238,11 @@ async function handleResponse<R = undefined>(response: Response): Promise<ApiRes
|
|||||||
message: text || '请求失败',
|
message: text || '请求失败',
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('响应成功', `type=text`, `text=${text}`)
|
||||||
return {
|
return {
|
||||||
success: true,
|
success: true,
|
||||||
data: undefined as unknown as R, // 强转类型,考虑优化
|
data: undefined as R, // 强转类型,考虑优化
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -327,7 +254,6 @@ async function handleResponse<R = undefined>(response: Response): Promise<ApiRes
|
|||||||
|
|
||||||
// 导出
|
// 导出
|
||||||
export {
|
export {
|
||||||
getDeviceToken,
|
|
||||||
getUserToken,
|
getUserToken,
|
||||||
callByDevice,
|
callByDevice,
|
||||||
callByUser,
|
callByUser,
|
||||||
|
|||||||
@@ -21,12 +21,13 @@ import zod from 'zod'
|
|||||||
import Captcha from './captcha'
|
import Captcha from './captcha'
|
||||||
import verify from '@/actions/auth/verify'
|
import verify from '@/actions/auth/verify'
|
||||||
import {login} from '@/actions/auth/auth'
|
import {login} from '@/actions/auth/auth'
|
||||||
import {useRouter} from 'next/navigation'
|
import {useRouter, useSearchParams} from 'next/navigation'
|
||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
import {ApiResponse} from '@/lib/api'
|
import {ApiResponse} from '@/lib/api'
|
||||||
import {Label} from '@/components/ui/label'
|
import {Label} from '@/components/ui/label'
|
||||||
import logo from '@/assets/logo.webp'
|
import logo from '@/assets/logo.webp'
|
||||||
import bg from './_assets/bg.webp'
|
import bg from './_assets/bg.webp'
|
||||||
|
import {useProfileStore} from '@/components/providers/StoreProvider'
|
||||||
|
|
||||||
export type LoginPageProps = {}
|
export type LoginPageProps = {}
|
||||||
|
|
||||||
@@ -157,30 +158,27 @@ export default function LoginPage(props: LoginPageProps) {
|
|||||||
// 调用登录函数
|
// 调用登录函数
|
||||||
const result = await login({
|
const result = await login({
|
||||||
username: values.username,
|
username: values.username,
|
||||||
password: values.password, // 使用验证码作为密码
|
password: values.password,
|
||||||
remember: values.remember,
|
remember: values.remember,
|
||||||
})
|
})
|
||||||
|
|
||||||
if (result.success) {
|
// 登录失败
|
||||||
// 登录成功
|
if (!result.success) {
|
||||||
toast.success('登录成功', {
|
return toast.error(result.message, {
|
||||||
description: '欢迎回来!',
|
|
||||||
})
|
|
||||||
|
|
||||||
// 跳转到首页或用户仪表板
|
|
||||||
router.push('/')
|
|
||||||
router.refresh() // 刷新页面状态
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// 登录失败
|
|
||||||
toast.error(result.message, {
|
|
||||||
description: '请检查您的手机号码和验证码',
|
description: '请检查您的手机号码和验证码',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 登录成功
|
||||||
|
await refreshProfile()
|
||||||
|
router.push(redirect || '/')
|
||||||
|
toast.success('登录成功', {
|
||||||
|
description: '欢迎回来!',
|
||||||
|
})
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
toast.error('服务器错误', {
|
toast.error('登录错误', {
|
||||||
description: '请稍后再试',
|
description: (e as Error).message,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
@@ -188,6 +186,19 @@ export default function LoginPage(props: LoginPageProps) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// 重定向
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
const params = useSearchParams()
|
||||||
|
const redirect = params.get('redirect')
|
||||||
|
|
||||||
|
const refreshProfile = useProfileStore(store=>store.refreshProfile)
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// render
|
||||||
|
// ======================
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<main className={merge(
|
<main className={merge(
|
||||||
`relative`,
|
`relative`,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
import {createContext, ReactNode, useCallback, useEffect, useMemo, useState} from 'react'
|
import {createContext, ReactNode, useCallback, useContext, useEffect, useMemo, useState} from 'react'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import {LinkItem, MenuItem} from './navs'
|
import {LinkItem, MenuItem} from './navs'
|
||||||
@@ -8,14 +8,14 @@ import ProductMenu from './product'
|
|||||||
import HelpMenu from './help'
|
import HelpMenu from './help'
|
||||||
import Wrap from '@/components/wrap'
|
import Wrap from '@/components/wrap'
|
||||||
import logo from '@/assets/logo.webp'
|
import logo from '@/assets/logo.webp'
|
||||||
|
import {Button} from '@/components/ui/button'
|
||||||
|
import {useProfileStore} from '@/components/providers/StoreProvider'
|
||||||
|
|
||||||
export const HeaderContext = createContext<{
|
export const HeaderContext = createContext<{
|
||||||
setMenu: (value: boolean) => void
|
setMenu: (value: boolean) => void
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
export type ProviderProps = {
|
export type ProviderProps = {}
|
||||||
userCenter: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function Provider(props: ProviderProps) {
|
export default function Provider(props: ProviderProps) {
|
||||||
|
|
||||||
@@ -51,7 +51,13 @@ export default function Provider(props: ProviderProps) {
|
|||||||
], [])
|
], [])
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// 渲染组件
|
// 用户信息
|
||||||
|
// ======================
|
||||||
|
|
||||||
|
const profile = useProfileStore(store=>store.profile)
|
||||||
|
|
||||||
|
// ======================
|
||||||
|
// render
|
||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -116,7 +122,35 @@ export default function Provider(props: ProviderProps) {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
{/* 登录 */}
|
{/* 登录 */}
|
||||||
{props.userCenter}
|
<div className={`flex items-center`}>
|
||||||
|
{profile == undefined
|
||||||
|
? <>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className={`w-24 h-12 flex items-center justify-center lg:text-lg`}
|
||||||
|
>
|
||||||
|
<span>登录</span>
|
||||||
|
</Link>
|
||||||
|
<Link
|
||||||
|
href="/login"
|
||||||
|
className={[
|
||||||
|
`w-20 lg:w-24 h-10 lg:h-12 bg-gradient-to-r rounded-sm flex items-center justify-center lg:text-lg text-white`,
|
||||||
|
`transition-colors duration-200 ease-in-out`,
|
||||||
|
`from-blue-500 to-cyan-400 hover:from-blue-500 hover:to-cyan-300`,
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
<span>注册</span>
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
: (
|
||||||
|
<Link href={`/admin`}>
|
||||||
|
<Button theme={`gradient`}>
|
||||||
|
进入控制台
|
||||||
|
</Button>
|
||||||
|
</Link>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
</div>
|
||||||
</Wrap>
|
</Wrap>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import Link from 'next/link'
|
|
||||||
import {cookies} from 'next/headers'
|
|
||||||
import {Button} from '@/components/ui/button'
|
|
||||||
import {AuthContext} from '@/lib/auth'
|
|
||||||
|
|
||||||
export type UserCenterProps = {}
|
|
||||||
|
|
||||||
export default async function UserCenter(props: UserCenterProps) {
|
|
||||||
|
|
||||||
const store = await cookies()
|
|
||||||
const info = store.get('auth_info')?.value
|
|
||||||
const data = info ? JSON.parse(info) as AuthContext : undefined
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`flex items-center`}>
|
|
||||||
{data == undefined
|
|
||||||
? <>
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className={`w-24 h-12 flex items-center justify-center lg:text-lg`}
|
|
||||||
>
|
|
||||||
<span>登录</span>
|
|
||||||
</Link>
|
|
||||||
<Link
|
|
||||||
href="/login"
|
|
||||||
className={[
|
|
||||||
`w-20 lg:w-24 h-10 lg:h-12 bg-gradient-to-r rounded-sm flex items-center justify-center lg:text-lg text-white`,
|
|
||||||
`transition-colors duration-200 ease-in-out`,
|
|
||||||
`from-blue-500 to-cyan-400 hover:from-blue-500 hover:to-cyan-300`,
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
<span>注册</span>
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
: (
|
|
||||||
<Link href={`/admin`}>
|
|
||||||
<Button theme={`gradient`}>
|
|
||||||
进入控制台
|
|
||||||
</Button>
|
|
||||||
</Link>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,14 +1,11 @@
|
|||||||
import Provider from '@/app/(root)/@header/_client/provider'
|
import Provider from '@/app/(root)/@header/_client/provider'
|
||||||
import UserCenter from '@/app/(root)/@header/_server/user-center'
|
|
||||||
|
|
||||||
export type HeaderProps = {}
|
export type HeaderProps = {}
|
||||||
|
|
||||||
export default async function Header(props: HeaderProps) {
|
export default async function Header(props: HeaderProps) {
|
||||||
return (
|
return (
|
||||||
<header className={`fixed top-0 w-full z-10`}>
|
<header className={`fixed top-0 w-full z-10`}>
|
||||||
<Provider
|
<Provider/>
|
||||||
userCenter={<UserCenter/>}
|
|
||||||
/>
|
|
||||||
</header>
|
</header>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,6 @@ import {Tabs, TabsContent, TabsList, TabsTrigger} from '@/components/ui/tabs'
|
|||||||
export type DashboardPageProps = {}
|
export type DashboardPageProps = {}
|
||||||
|
|
||||||
export default async function DashboardPage(props: DashboardPageProps) {
|
export default async function DashboardPage(props: DashboardPageProps) {
|
||||||
|
|
||||||
const profile = await getProfile()
|
|
||||||
if (!profile) {
|
|
||||||
return redirect('/login')
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Page className={`flex-auto grid grid-cols-4 grid-rows-[150px_minmax(200px,1fr)_minmax(200px,1fr)_minmax(200px,1fr)]`}>
|
<Page className={`flex-auto grid grid-cols-4 grid-rows-[150px_minmax(200px,1fr)_minmax(200px,1fr)_minmax(200px,1fr)]`}>
|
||||||
{/* banner */}
|
{/* banner */}
|
||||||
|
|||||||
@@ -1,17 +1,33 @@
|
|||||||
import {cookies} from 'next/headers'
|
'use client'
|
||||||
import {Button} from '@/components/ui/button'
|
import {Button} from '@/components/ui/button'
|
||||||
import {logout} from '@/actions/auth/auth'
|
import {logout} from '@/actions/auth/auth'
|
||||||
|
import {useProfileStore} from '@/components/providers/StoreProvider'
|
||||||
|
import {useRouter} from 'next/navigation'
|
||||||
|
import {toast} from 'sonner'
|
||||||
|
|
||||||
export type ProfileProps = {}
|
export type ProfileProps = {}
|
||||||
|
|
||||||
export default async function Profile(props: ProfileProps) {
|
export default function Profile(props: ProfileProps) {
|
||||||
const store = await cookies()
|
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||||
const info = store.get('auth_info')?.value
|
const router = useRouter()
|
||||||
const data = info ? JSON.parse(info) : undefined
|
const doLogout = async () => {
|
||||||
|
try {
|
||||||
|
const resp = await logout()
|
||||||
|
if (resp.success) {
|
||||||
|
await refreshProfile()
|
||||||
|
router.push('/')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (e) {
|
||||||
|
toast.error('退出登录失败', {
|
||||||
|
description: (e as Error).message,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex gap-2 items-center">
|
<div className="flex gap-2 items-center">
|
||||||
<span>下午好,{data?.payload.name}</span>
|
<Button theme={`error`} onClick={doLogout}>
|
||||||
<Button theme={`error`} onClick={logout}>
|
|
||||||
退出登录
|
退出登录
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ export default function BillsPage(props: BillsPageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
console.log('init bill list')
|
||||||
|
refresh(1, 10).then()
|
||||||
refresh(1, 10).then()
|
refresh(1, 10).then()
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {Identify} from '@/actions/auth/identify'
|
|||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
import {useContext, useEffect, useRef, useState} from 'react'
|
import {useContext, useEffect, useRef, useState} from 'react'
|
||||||
import * as qrcode from 'qrcode'
|
import * as qrcode from 'qrcode'
|
||||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
import {StoreContext, useProfileStore} from '@/components/providers/StoreProvider'
|
||||||
|
|
||||||
export type IdentifyPageProps = {}
|
export type IdentifyPageProps = {}
|
||||||
|
|
||||||
@@ -90,8 +90,8 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
|||||||
// 用户数据
|
// 用户数据
|
||||||
// ======================
|
// ======================
|
||||||
|
|
||||||
const ctx = useContext(AuthContext)
|
const profile = useProfileStore(store => store.profile)
|
||||||
console.log('render identify page')
|
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||||
|
|
||||||
// ======================
|
// ======================
|
||||||
// render
|
// render
|
||||||
@@ -116,7 +116,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
|||||||
<h3 className={`text-center text-lg font-bold`}>个人认证</h3>
|
<h3 className={`text-center text-lg font-bold`}>个人认证</h3>
|
||||||
<p className={`text-sm text-gray-600`}>平台授权支付宝安全认证,不会泄露您的认证信息</p>
|
<p className={`text-sm text-gray-600`}>平台授权支付宝安全认证,不会泄露您的认证信息</p>
|
||||||
</div>
|
</div>
|
||||||
{ctx.profile?.id_token ? (
|
{profile?.id_token ? (
|
||||||
<div className={`flex flex-col gap-4`}>
|
<div className={`flex flex-col gap-4`}>
|
||||||
<p className={`text-sm text-gray-600`}>已完成实名认证</p>
|
<p className={`text-sm text-gray-600`}>已完成实名认证</p>
|
||||||
</div>
|
</div>
|
||||||
@@ -162,7 +162,7 @@ export default function IdentifyPage(props: IdentifyPageProps) {
|
|||||||
<canvas ref={canvas} width={256} height={256}/>
|
<canvas ref={canvas} width={256} height={256}/>
|
||||||
<p className={`text-sm text-gray-600`}>请扫码完成认证</p>
|
<p className={`text-sm text-gray-600`}>请扫码完成认证</p>
|
||||||
<Button onClick={async () => {
|
<Button onClick={async () => {
|
||||||
await ctx.refreshProfile()
|
await refreshProfile()
|
||||||
setOpenDialog(false)
|
setOpenDialog(false)
|
||||||
}}>
|
}}>
|
||||||
已完成认证
|
已完成认证
|
||||||
|
|||||||
@@ -4,8 +4,8 @@ import logo from '@/assets/logo.webp'
|
|||||||
import Profile from '@/app/admin/_server/profile'
|
import Profile from '@/app/admin/_server/profile'
|
||||||
import {merge} from '@/lib/utils'
|
import {merge} from '@/lib/utils'
|
||||||
import Link from 'next/link'
|
import Link from 'next/link'
|
||||||
import {getProfile} from '@/actions/auth/auth'
|
|
||||||
import {redirect} from 'next/navigation'
|
import {redirect} from 'next/navigation'
|
||||||
|
import {getProfile} from '@/actions/auth/auth'
|
||||||
|
|
||||||
export type DashboardLayoutProps = {
|
export type DashboardLayoutProps = {
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
@@ -13,9 +13,9 @@ export type DashboardLayoutProps = {
|
|||||||
|
|
||||||
export default async function DashboardLayout(props: DashboardLayoutProps) {
|
export default async function DashboardLayout(props: DashboardLayoutProps) {
|
||||||
|
|
||||||
const profile = await getProfile()
|
const user = await getProfile()
|
||||||
if (!profile) {
|
if (!user) {
|
||||||
return redirect('/login')
|
return redirect(`/login?redirect=${encodeURIComponent('/admin')}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {Input} from '@/components/ui/input'
|
|||||||
import {useForm} from 'react-hook-form'
|
import {useForm} from 'react-hook-form'
|
||||||
import {zodResolver} from '@hookform/resolvers/zod'
|
import {zodResolver} from '@hookform/resolvers/zod'
|
||||||
import * as z from 'zod'
|
import * as z from 'zod'
|
||||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
import {StoreContext, useProfileStore} from '@/components/providers/StoreProvider'
|
||||||
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
|
import {Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger} from '@/components/ui/dialog'
|
||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
import {Alert, AlertDescription, AlertTitle} from '@/components/ui/alert'
|
import {Alert, AlertDescription, AlertTitle} from '@/components/ui/alert'
|
||||||
@@ -22,8 +22,8 @@ import {User} from '@/lib/models'
|
|||||||
export type ProfilePageProps = {}
|
export type ProfilePageProps = {}
|
||||||
|
|
||||||
export default function ProfilePage(props: ProfilePageProps) {
|
export default function ProfilePage(props: ProfilePageProps) {
|
||||||
const authCtx = useContext(AuthContext)
|
const profile = useProfileStore(store => store.profile)
|
||||||
const profile = authCtx.profile
|
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||||
|
|
||||||
// 默认选中的Tab
|
// 默认选中的Tab
|
||||||
const [activeTab, setActiveTab] = useState('basic')
|
const [activeTab, setActiveTab] = useState('basic')
|
||||||
@@ -55,19 +55,19 @@ export default function ProfilePage(props: ProfilePageProps) {
|
|||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<TabsContent value="basic">
|
<TabsContent value="basic">
|
||||||
<BasicInfoTab profile={profile} refreshProfile={authCtx.refreshProfile}/>
|
<BasicInfoTab profile={profile} refreshProfile={refreshProfile}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="security">
|
<TabsContent value="security">
|
||||||
<SecurityTab profile={profile} refreshProfile={authCtx.refreshProfile}/>
|
<SecurityTab profile={profile} refreshProfile={refreshProfile}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="balance">
|
<TabsContent value="balance">
|
||||||
<BalanceTab profile={profile} refreshProfile={authCtx.refreshProfile}/>
|
<BalanceTab profile={profile} refreshProfile={refreshProfile}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
<TabsContent value="identify">
|
<TabsContent value="identify">
|
||||||
<IdentifyTab profile={profile} refreshProfile={authCtx.refreshProfile}/>
|
<IdentifyTab profile={profile} refreshProfile={refreshProfile}/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import {Metadata} from 'next'
|
|||||||
import './globals.css'
|
import './globals.css'
|
||||||
import localFont from 'next/font/local'
|
import localFont from 'next/font/local'
|
||||||
import {Toaster} from '@/components/ui/sonner'
|
import {Toaster} from '@/components/ui/sonner'
|
||||||
import AuthProvider from '@/components/providers/AuthProvider'
|
import StoreProvider from '@/components/providers/StoreProvider'
|
||||||
import {getProfile} from '@/actions/auth/auth'
|
import {getProfile} from '@/actions/auth/auth'
|
||||||
|
|
||||||
const font = localFont({
|
const font = localFont({
|
||||||
@@ -20,12 +20,15 @@ export default async function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: ReactNode
|
children: ReactNode
|
||||||
}>) {
|
}>) {
|
||||||
|
|
||||||
|
const user = await getProfile()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="zh-Cn">
|
<html lang="zh-Cn">
|
||||||
<body className={`${font.className}`}>
|
<body className={`${font.className}`}>
|
||||||
<AuthProvider>
|
<StoreProvider user={user}>
|
||||||
{children}
|
{children}
|
||||||
</AuthProvider>
|
</StoreProvider>
|
||||||
<Toaster position={'top-center'} richColors expand/>
|
<Toaster position={'top-center'} richColors expand/>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {zodResolver} from '@hookform/resolvers/zod'
|
|||||||
import {createResourceByBalance} from '@/actions/resource'
|
import {createResourceByBalance} from '@/actions/resource'
|
||||||
import {toast} from 'sonner'
|
import {toast} from 'sonner'
|
||||||
import {useRouter} from 'next/navigation'
|
import {useRouter} from 'next/navigation'
|
||||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
import {StoreContext} from '@/components/providers/StoreProvider'
|
||||||
|
|
||||||
// 定义表单验证架构
|
// 定义表单验证架构
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import wechat from '../_assets/wechat.svg'
|
|||||||
import balance from '../_assets/balance.svg'
|
import balance from '../_assets/balance.svg'
|
||||||
import Image from 'next/image'
|
import Image from 'next/image'
|
||||||
import {useContext, useRef, useState} from 'react'
|
import {useContext, useRef, useState} from 'react'
|
||||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
import {StoreContext, useProfileStore} from '@/components/providers/StoreProvider'
|
||||||
import {Alert, AlertDescription} from '@/components/ui/alert'
|
import {Alert, AlertDescription} from '@/components/ui/alert'
|
||||||
import {
|
import {
|
||||||
prepareResourceByAlipay,
|
prepareResourceByAlipay,
|
||||||
@@ -32,7 +32,9 @@ export type PayProps = {
|
|||||||
|
|
||||||
export default function Pay(props: PayProps) {
|
export default function Pay(props: PayProps) {
|
||||||
|
|
||||||
const ctx = useContext(AuthContext)
|
const profile = useProfileStore(store=>store.profile)
|
||||||
|
const refreshProfile = useProfileStore(store=>store.refreshProfile)
|
||||||
|
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const [payInfo, setPayInfo] = useState<CreateResourceResp | undefined>()
|
const [payInfo, setPayInfo] = useState<CreateResourceResp | undefined>()
|
||||||
const canvas = useRef<HTMLCanvasElement>(null)
|
const canvas = useRef<HTMLCanvasElement>(null)
|
||||||
@@ -97,7 +99,7 @@ export default function Pay(props: PayProps) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
setOpen(false)
|
setOpen(false)
|
||||||
await ctx.refreshProfile()
|
await refreshProfile()
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
console.log(e)
|
console.log(e)
|
||||||
@@ -133,12 +135,12 @@ export default function Pay(props: PayProps) {
|
|||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
|
||||||
{props.method === 'balance' ? (
|
{props.method === 'balance' ? (
|
||||||
ctx.profile && (
|
profile && (
|
||||||
<div className="flex flex-col gap-4">
|
<div className="flex flex-col gap-4">
|
||||||
<div className="flex flex-col gap-1">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-weak text-sm">账户余额</span>
|
<span className="text-weak text-sm">账户余额</span>
|
||||||
<span className={`text-lg`}>{ctx.profile.balance}元</span>
|
<span className={`text-lg`}>{profile.balance}元</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-weak text-sm">支付金额</span>
|
<span className="text-weak text-sm">支付金额</span>
|
||||||
@@ -147,13 +149,13 @@ export default function Pay(props: PayProps) {
|
|||||||
<hr className="my-2"/>
|
<hr className="my-2"/>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<span className="text-weak text-sm">支付后余额</span>
|
<span className="text-weak text-sm">支付后余额</span>
|
||||||
<span className={`text-lg ${ctx.profile.balance > props.amount ? 'text-done' : `text-fail`}`}>
|
<span className={`text-lg ${profile.balance > props.amount ? 'text-done' : `text-fail`}`}>
|
||||||
{ctx.profile.balance - props.amount}元
|
{profile.balance - props.amount}元
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{ctx.profile.balance < props.amount && (
|
{profile.balance < props.amount && (
|
||||||
<Alert variant="fail">
|
<Alert variant="fail">
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
余额不足,请先充值或选择其他支付方式
|
余额不足,请先充值或选择其他支付方式
|
||||||
@@ -161,7 +163,7 @@ export default function Pay(props: PayProps) {
|
|||||||
</Alert>
|
</Alert>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{ctx.profile.balance >= props.amount && (
|
{profile.balance >= props.amount && (
|
||||||
<Alert>
|
<Alert>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
检查无误后,点击确认支付按钮完成支付
|
检查无误后,点击确认支付按钮完成支付
|
||||||
@@ -200,7 +202,7 @@ export default function Pay(props: PayProps) {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
disabled={props.method === 'balance' && !!ctx.profile && ctx.profile.balance < props.amount}
|
disabled={props.method === 'balance' && !!profile && profile.balance < props.amount}
|
||||||
onClick={onSubmit}
|
onClick={onSubmit}
|
||||||
>
|
>
|
||||||
{props.method === 'balance' ? '确认支付' : '已完成支付'}
|
{props.method === 'balance' ? '确认支付' : '已完成支付'}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ import {useContext, useRef, useState} from 'react'
|
|||||||
import {Loader} from 'lucide-react'
|
import {Loader} from 'lucide-react'
|
||||||
import {RechargeByAlipay, RechargeByAlipayConfirm, RechargeByWechat, RechargeByWechatConfirm} from '@/actions/user'
|
import {RechargeByAlipay, RechargeByAlipayConfirm, RechargeByWechat, RechargeByWechatConfirm} from '@/actions/user'
|
||||||
import * as qrcode from 'qrcode'
|
import * as qrcode from 'qrcode'
|
||||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
import {StoreContext, useProfileStore} from '@/components/providers/StoreProvider'
|
||||||
|
|
||||||
const schema = zod.object({
|
const schema = zod.object({
|
||||||
method: zod.enum(['alipay', 'wechat']),
|
method: zod.enum(['alipay', 'wechat']),
|
||||||
@@ -53,7 +53,7 @@ export default function RechargeModal(props: RechargeModelProps) {
|
|||||||
pay_url: string
|
pay_url: string
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const ctx = useContext(AuthContext)
|
const refreshProfile = useProfileStore(store => store.refreshProfile)
|
||||||
|
|
||||||
const createRecharge = async (data: Schema) => {
|
const createRecharge = async (data: Schema) => {
|
||||||
try {
|
try {
|
||||||
@@ -124,7 +124,7 @@ export default function RechargeModal(props: RechargeModelProps) {
|
|||||||
}
|
}
|
||||||
toast.success(`充值成功`)
|
toast.success(`充值成功`)
|
||||||
closeDialog()
|
closeDialog()
|
||||||
await ctx.refreshProfile()
|
await refreshProfile()
|
||||||
}
|
}
|
||||||
catch (e) {
|
catch (e) {
|
||||||
toast.error(`充值失败`, {
|
toast.error(`充值失败`, {
|
||||||
|
|||||||
@@ -8,15 +8,16 @@ import Image from 'next/image'
|
|||||||
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
import alipay from '@/components/composites/purchase/_assets/alipay.svg'
|
||||||
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
import wechat from '@/components/composites/purchase/_assets/wechat.svg'
|
||||||
import balance from '@/components/composites/purchase/_assets/balance.svg'
|
import balance from '@/components/composites/purchase/_assets/balance.svg'
|
||||||
import {AuthContext} from '@/components/providers/AuthProvider'
|
import {useProfileStore} from '@/components/providers/StoreProvider'
|
||||||
import RechargeModal from '@/components/composites/purchase/_client/recharge'
|
import RechargeModal from '@/components/composites/purchase/_client/recharge'
|
||||||
import Pay from '@/components/composites/purchase/_client/pay'
|
import Pay from '@/components/composites/purchase/_client/pay'
|
||||||
|
import {Button, buttonVariants} from '@/components/ui/button'
|
||||||
|
import Link from 'next/link'
|
||||||
|
|
||||||
export type RightProps = {}
|
export type RightProps = {}
|
||||||
|
|
||||||
export default function Right(props: RightProps) {
|
export default function Right(props: RightProps) {
|
||||||
const authCtx = useContext(AuthContext)
|
const profile = useProfileStore(store => store.profile)
|
||||||
const profile = authCtx.profile
|
|
||||||
|
|
||||||
const form = useContext(PurchaseFormContext)?.form
|
const form = useContext(PurchaseFormContext)?.form
|
||||||
if (!form) {
|
if (!form) {
|
||||||
@@ -89,58 +90,65 @@ export default function Right(props: RightProps) {
|
|||||||
<span>价格</span>
|
<span>价格</span>
|
||||||
<span className={`text-xl text-orange-500`}>¥{price}</span>
|
<span className={`text-xl text-orange-500`}>¥{price}</span>
|
||||||
</p>
|
</p>
|
||||||
<FormField name={`pay_type`} label={`支付方式`} className={`flex flex-col gap-6`}>
|
{profile ? <>
|
||||||
{({id, field}) => (
|
<FormField name={`pay_type`} label={`支付方式`} className={`flex flex-col gap-6`}>
|
||||||
<RadioGroup
|
{({id, field}) => (
|
||||||
id={id}
|
<RadioGroup
|
||||||
defaultValue={field.value}
|
id={id}
|
||||||
onValueChange={field.onChange}
|
defaultValue={field.value}
|
||||||
className={`flex flex-col gap-3`}>
|
onValueChange={field.onChange}
|
||||||
<div className={`w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md`}>
|
className={`flex flex-col gap-3`}>
|
||||||
<p className={`flex items-center gap-3`}>
|
|
||||||
<Image src={balance} alt={`余额icon`}/>
|
|
||||||
<span className={`text-sm text-gray-500`}>账户余额</span>
|
|
||||||
</p>
|
|
||||||
<p className={`flex justify-between items-center`}>
|
|
||||||
<span className={`text-xl`}>{profile?.balance}</span>
|
|
||||||
<RechargeModal/>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
</div>
|
<div className={`w-full p-3 flex flex-col gap-4 bg-gray-100 rounded-md`}>
|
||||||
<FormOption
|
<p className={`flex items-center gap-3`}>
|
||||||
id={`${id}-balance`}
|
<Image src={balance} alt={`余额icon`}/>
|
||||||
value={`balance`}
|
<span className={`text-sm text-gray-500`}>账户余额</span>
|
||||||
compare={field.value}
|
</p>
|
||||||
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
<p className={`flex justify-between items-center`}>
|
||||||
<Image src={balance} alt={`余额 icon`}/>
|
<span className={`text-xl`}>{profile?.balance}</span>
|
||||||
<span>余额</span>
|
<RechargeModal/>
|
||||||
</FormOption>
|
</p>
|
||||||
<FormOption
|
</div>
|
||||||
id={`${id}-wechat`}
|
|
||||||
value={`wechat`}
|
<FormOption
|
||||||
compare={field.value}
|
id={`${id}-balance`}
|
||||||
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
value={`balance`}
|
||||||
<Image src={wechat} alt={`微信 logo`}/>
|
compare={field.value}
|
||||||
<span>微信</span>
|
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
||||||
</FormOption>
|
<Image src={balance} alt={`余额 icon`}/>
|
||||||
<FormOption
|
<span>余额</span>
|
||||||
id={`${id}-alipay`}
|
</FormOption>
|
||||||
value={`alipay`}
|
<FormOption
|
||||||
compare={field.value}
|
id={`${id}-wechat`}
|
||||||
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
value={`wechat`}
|
||||||
<Image src={alipay} alt={`支付宝 logo`}/>
|
compare={field.value}
|
||||||
<span>支付宝</span>
|
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
||||||
</FormOption>
|
<Image src={wechat} alt={`微信 logo`}/>
|
||||||
</RadioGroup>
|
<span>微信</span>
|
||||||
)}
|
</FormOption>
|
||||||
</FormField>
|
<FormOption
|
||||||
<Pay method={payType} amount={price} resource={{
|
id={`${id}-alipay`}
|
||||||
type: Number(watchType),
|
value={`alipay`}
|
||||||
live: Number(watchLive) * 60,
|
compare={field.value}
|
||||||
quota: watchQuota,
|
className={`p-3 w-full flex-row gap-2 justify-center`}>
|
||||||
expire: Number(watchExpire),
|
<Image src={alipay} alt={`支付宝 logo`}/>
|
||||||
daily_limit: watchDailyLimit,
|
<span>支付宝</span>
|
||||||
}}/>
|
</FormOption>
|
||||||
|
</RadioGroup>
|
||||||
|
)}
|
||||||
|
</FormField>
|
||||||
|
<Pay method={payType} amount={price} resource={{
|
||||||
|
type: Number(watchType),
|
||||||
|
live: Number(watchLive) * 60,
|
||||||
|
quota: watchQuota,
|
||||||
|
expire: Number(watchExpire),
|
||||||
|
daily_limit: watchDailyLimit,
|
||||||
|
}}/>
|
||||||
|
</> : (
|
||||||
|
<Link href={`/login`} className={buttonVariants()}>
|
||||||
|
登录后支付
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
'use client'
|
|
||||||
import {User} from '@/lib/models'
|
|
||||||
import {createContext, ReactNode, useEffect, useState} from 'react'
|
|
||||||
import {getProfile} from '@/actions/auth/auth'
|
|
||||||
|
|
||||||
type AuthContentType = {
|
|
||||||
profile: User | null
|
|
||||||
refreshProfile: () => Promise<void>
|
|
||||||
}
|
|
||||||
|
|
||||||
export const AuthContext = createContext<AuthContentType>({
|
|
||||||
profile: null,
|
|
||||||
refreshProfile: async () => {
|
|
||||||
throw new Error('Not implemented')
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
export type ProfileProviderProps = {
|
|
||||||
children: ReactNode
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AuthProvider(props: ProfileProviderProps) {
|
|
||||||
|
|
||||||
const [profile, setProfile] = useState<User | null>(null)
|
|
||||||
|
|
||||||
const refreshProfile = async () => {
|
|
||||||
setProfile(await getProfile(true))
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
refreshProfile().then()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<AuthContext.Provider value={{
|
|
||||||
profile, refreshProfile,
|
|
||||||
}}>
|
|
||||||
{props.children}
|
|
||||||
</AuthContext.Provider>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
43
src/components/providers/StoreProvider.tsx
Normal file
43
src/components/providers/StoreProvider.tsx
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
'use client'
|
||||||
|
import {User} from '@/lib/models'
|
||||||
|
import {createContext, ReactNode, useContext, useRef} from 'react'
|
||||||
|
import {createProfileStore, ProfileStore} from '@/stores/profile-store'
|
||||||
|
import {StoreApi} from 'zustand/vanilla'
|
||||||
|
import {useStore} from 'zustand/react'
|
||||||
|
|
||||||
|
|
||||||
|
export type StoreContextType = {
|
||||||
|
profile: StoreApi<ProfileStore>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoreContext = createContext<StoreContextType | null>(null)
|
||||||
|
|
||||||
|
export type ProfileProviderProps = {
|
||||||
|
user: User | null
|
||||||
|
children: ReactNode
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function StoreProvider(props: ProfileProviderProps) {
|
||||||
|
const profile = useRef<StoreApi<ProfileStore>>(null)
|
||||||
|
if (!profile.current) {
|
||||||
|
console.log('create profile store')
|
||||||
|
profile.current = createProfileStore(props.user)
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StoreContext.Provider value={{
|
||||||
|
profile: profile.current,
|
||||||
|
}}>
|
||||||
|
{props.children}
|
||||||
|
</StoreContext.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export function useProfileStore<T>(selector: (store: ProfileStore) => T) {
|
||||||
|
const ctx = useContext(StoreContext)
|
||||||
|
if (!ctx) {
|
||||||
|
throw new Error('useProfileStore must be used within a StoreProvider')
|
||||||
|
}
|
||||||
|
return useStore(ctx.profile, selector)
|
||||||
|
}
|
||||||
25
src/stores/profile-store.ts
Normal file
25
src/stores/profile-store.ts
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
import {User} from '@/lib/models'
|
||||||
|
import {createStore} from 'zustand/vanilla'
|
||||||
|
import {getProfile} from '@/actions/auth/auth'
|
||||||
|
|
||||||
|
|
||||||
|
export type ProfileStore = ProfileState & ProfileActions
|
||||||
|
|
||||||
|
export type ProfileState = {
|
||||||
|
profile: User | null
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ProfileActions = {
|
||||||
|
refreshProfile: () => Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createProfileStore = (init: User|null) => {
|
||||||
|
return createStore<ProfileStore>()(setState => ({
|
||||||
|
profile: init,
|
||||||
|
refreshProfile: async () => {
|
||||||
|
const profile = await getProfile()
|
||||||
|
setState({profile})
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
Reference in New Issue
Block a user