@wzyjs/cli 0.3.32 → 0.3.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +350 -35
- package/package.json +6 -4
- package/template/web/base/.next/cache/tsconfig.tsbuildinfo +1 -0
- package/template/web/base/Dockerfile +37 -0
- package/template/web/base/_env +2 -0
- package/template/web/base/eslint.config.js +94 -0
- package/template/web/base/next.config.js +29 -0
- package/template/web/base/package.json +53 -0
- package/template/web/base/postcss.config.js +5 -0
- package/template/web/base/prisma/models/demo/DemoItem.zmodel +8 -0
- package/template/web/base/prisma/schema.prisma +29 -0
- package/template/web/base/prisma/schema.zmodel +1 -0
- package/template/web/base/src/app/api/trpc/[trpc]/route.ts +24 -0
- package/template/web/base/src/app/auth/disabled/page.tsx +29 -0
- package/template/web/base/src/app/auth/error/page.tsx +47 -0
- package/template/web/base/src/app/auth/signin/page.tsx +53 -0
- package/template/web/base/src/app/auth/unauthorized/page.tsx +60 -0
- package/template/web/base/src/app/demo/page.tsx +59 -0
- package/template/web/base/src/app/layout.tsx +32 -0
- package/template/web/base/src/app/not-found.tsx +21 -0
- package/template/web/base/src/app/page.tsx +92 -0
- package/template/web/base/src/components/base/Layout/Header/context.tsx +56 -0
- package/template/web/base/src/components/base/Layout/Header/index.tsx +32 -0
- package/template/web/base/src/components/base/Layout/index.tsx +27 -0
- package/template/web/base/src/components/base/Providers/TRPCReactProvider.tsx +25 -0
- package/template/web/base/src/components/base/Providers/index.tsx +31 -0
- package/template/web/base/src/components/base/display/DisplayProvider.tsx +44 -0
- package/template/web/base/src/components/base/display/consts.ts +10 -0
- package/template/web/base/src/components/base/display/context.ts +6 -0
- package/template/web/base/src/components/base/display/index.ts +4 -0
- package/template/web/base/src/components/base/display/types.ts +12 -0
- package/template/web/base/src/components/base/display/useDisplay.ts +9 -0
- package/template/web/base/src/components/base/display/utils.ts +18 -0
- package/template/web/base/src/components/base/theme/ThemeProvider.tsx +83 -0
- package/template/web/base/src/components/base/theme/ThemeToggle.tsx +26 -0
- package/template/web/base/src/components/base/theme/consts.tsx +59 -0
- package/template/web/base/src/components/base/theme/context.ts +14 -0
- package/template/web/base/src/components/base/theme/useAppTheme.ts +15 -0
- package/template/web/base/src/components/base/theme/utils.ts +17 -0
- package/template/web/base/src/components/index.ts +1 -0
- package/template/web/base/src/consts/index.ts +6 -0
- package/template/web/base/src/enums/index.ts +1 -0
- package/template/web/base/src/env.js +44 -0
- package/template/web/base/src/hooks/index.ts +1 -0
- package/template/web/base/src/server/db/client.ts +19 -0
- package/template/web/base/src/server/routers/index.ts +3 -0
- package/template/web/base/src/server/trpc/context.ts +14 -0
- package/template/web/base/src/server/trpc/index.ts +1 -0
- package/template/web/base/src/server/trpc/init.ts +27 -0
- package/template/web/base/src/server/trpc/procedures.ts +5 -0
- package/template/web/base/src/server/trpc/router.ts +11 -0
- package/template/web/base/src/server/utils/index.ts +1 -0
- package/template/web/base/src/styles/globals.css +3 -0
- package/template/web/base/src/types/index.ts +1 -0
- package/template/web/base/src/utils/index.ts +4 -0
- package/template/web/base/src/utils/query-client/index.ts +31 -0
- package/template/web/base/src/utils/trpc/index.ts +23 -0
- package/template/web/base/src/utils/trpc/utils.ts +61 -0
- package/template/web/base/tailwind.config.cjs +7 -0
- package/template/web/base/tsconfig.json +46 -0
- package/template/web/google/_env +8 -0
- package/template/web/google/package.json +55 -0
- package/template/web/google/prisma/models/auth/Account.zmodel +26 -0
- package/template/web/google/prisma/models/auth/Session.zmodel +17 -0
- package/template/web/google/prisma/models/auth/User.zmodel +22 -0
- package/template/web/google/prisma/schema.zmodel +2 -0
- package/template/web/google/src/app/api/auth/[...nextauth]/route.ts +7 -0
- package/template/web/google/src/app/auth/error/page.tsx +56 -0
- package/template/web/google/src/app/auth/signin/page.tsx +83 -0
- package/template/web/google/src/app/layout.tsx +35 -0
- package/template/web/google/src/components/base/AuthGuard/Loading.tsx +19 -0
- package/template/web/google/src/components/base/AuthGuard/index.tsx +45 -0
- package/template/web/google/src/components/base/Layout/Header/UserAuthStatus.tsx +93 -0
- package/template/web/google/src/components/base/Layout/Header/index.tsx +34 -0
- package/template/web/google/src/components/base/Providers/index.tsx +34 -0
- package/template/web/google/src/env.js +52 -0
- package/template/web/google/src/server/auth/next-auth/adapter.ts +77 -0
- package/template/web/google/src/server/auth/next-auth/options.ts +53 -0
- package/template/web/google/src/server/trpc/context.ts +21 -0
- package/template/web/google/src/server/trpc/procedures.ts +16 -0
- package/template/web/middle/_env +10 -0
- package/template/web/middle/package.json +55 -0
- package/template/web/middle/src/app/api/auth/[...nextauth]/route.ts +7 -0
- package/template/web/middle/src/app/auth/disabled/page.tsx +71 -0
- package/template/web/middle/src/app/auth/error/page.tsx +56 -0
- package/template/web/middle/src/app/auth/signin/page.tsx +91 -0
- package/template/web/middle/src/app/auth/unauthorized/page.tsx +88 -0
- package/template/web/middle/src/app/layout.tsx +35 -0
- package/template/web/middle/src/app/middle/page.tsx +10 -0
- package/template/web/middle/src/app/page.tsx +20 -0
- package/template/web/middle/src/components/base/AuthGuard/Loading.tsx +19 -0
- package/template/web/middle/src/components/base/AuthGuard/index.tsx +45 -0
- package/template/web/middle/src/components/base/Layout/Header/UserAuthStatus.tsx +93 -0
- package/template/web/middle/src/components/base/Layout/Header/index.tsx +34 -0
- package/template/web/middle/src/components/base/Layout/Sidebar/index.tsx +103 -0
- package/template/web/middle/src/components/base/Layout/index.tsx +35 -0
- package/template/web/middle/src/components/base/Providers/MiddleProvider.tsx +33 -0
- package/template/web/middle/src/components/base/Providers/index.tsx +37 -0
- package/template/web/middle/src/env.js +57 -0
- package/template/web/middle/src/server/auth/next-auth/options.ts +26 -0
- package/template/web/middle/src/server/trpc/context.ts +22 -0
- package/template/web/middle/src/server/trpc/procedures.ts +16 -0
- package/template/web/middle/src/server/utils/index.ts +2 -0
- package/template/web/middle/src/server/utils/middle.ts +34 -0
- package/template/web/middle/src/types/index.ts +3 -0
- package/template/web/middle/src/utils/index.ts +5 -0
- package/template/web/middle/src/utils/middle.ts +28 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { PrismaClient } from '@prisma/client'
|
|
2
|
+
|
|
3
|
+
import { env } from '@/env'
|
|
4
|
+
|
|
5
|
+
const createPrismaClient = () => {
|
|
6
|
+
return new PrismaClient({
|
|
7
|
+
log: env.NODE_ENV === 'development' ? ['error', 'warn'] : ['error'],
|
|
8
|
+
})
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const globalForPrisma = globalThis as unknown as {
|
|
12
|
+
prisma: ReturnType<typeof createPrismaClient> | undefined
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const db = globalForPrisma.prisma ?? createPrismaClient()
|
|
16
|
+
|
|
17
|
+
if (env.NODE_ENV !== 'production') {
|
|
18
|
+
globalForPrisma.prisma = db
|
|
19
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { db } from '@/server/db/client'
|
|
2
|
+
|
|
3
|
+
import { enhance } from '../generated/enhancer/enhance'
|
|
4
|
+
|
|
5
|
+
export const createTRPCContext = async (opts?: { req?: Request }) => {
|
|
6
|
+
return {
|
|
7
|
+
// db, 严禁使用这个模块,而应该使用 prisma
|
|
8
|
+
req: opts?.req,
|
|
9
|
+
headers: opts?.req?.headers,
|
|
10
|
+
prisma: enhance(db),
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type TRPCContext = Awaited<ReturnType<typeof createTRPCContext>>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { apiRouter, type ApiRouter, type ApiRouterInput, type ApiRouterOutput } from './router'
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { initTRPC } from '@trpc/server'
|
|
2
|
+
import superjson from 'superjson'
|
|
3
|
+
import { ZodError } from 'zod'
|
|
4
|
+
import { fromZodError } from 'zod-validation-error'
|
|
5
|
+
|
|
6
|
+
import { type TRPCContext } from './context'
|
|
7
|
+
|
|
8
|
+
const t = initTRPC.context<TRPCContext>().create({
|
|
9
|
+
transformer: superjson,
|
|
10
|
+
errorFormatter({ shape, error }) {
|
|
11
|
+
return {
|
|
12
|
+
...shape,
|
|
13
|
+
data: {
|
|
14
|
+
...shape.data,
|
|
15
|
+
formattedError:
|
|
16
|
+
error.cause instanceof ZodError
|
|
17
|
+
? fromZodError(error.cause).message
|
|
18
|
+
: null,
|
|
19
|
+
},
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
export const createCallerFactory = t.createCallerFactory
|
|
25
|
+
export const createTRPCRouter = t.router
|
|
26
|
+
export const createTRPCProcedure = t.procedure
|
|
27
|
+
export const mergeTRPCRouters = t.mergeRouters
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { inferRouterInputs, inferRouterOutputs } from '@trpc/server'
|
|
2
|
+
|
|
3
|
+
import routes from '../routers'
|
|
4
|
+
|
|
5
|
+
export const apiRouter = routes
|
|
6
|
+
|
|
7
|
+
export type ApiRouter = typeof apiRouter
|
|
8
|
+
|
|
9
|
+
export type ApiRouterInput = inferRouterInputs<ApiRouter>
|
|
10
|
+
|
|
11
|
+
export type ApiRouterOutput = inferRouterOutputs<ApiRouter>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from '@wzyjs/utils/node'
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export type { ApiRouterInput, ApiRouterOutput } from '@/server/trpc'
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import SuperJSON from 'superjson'
|
|
2
|
+
import { QueryClient, defaultShouldDehydrateQuery } from '@tanstack/react-query'
|
|
3
|
+
|
|
4
|
+
let clientQueryClientSingleton: QueryClient | undefined = undefined
|
|
5
|
+
|
|
6
|
+
const createQueryClient = () => {
|
|
7
|
+
return new QueryClient({
|
|
8
|
+
defaultOptions: {
|
|
9
|
+
queries: {
|
|
10
|
+
staleTime: 30 * 1000,
|
|
11
|
+
},
|
|
12
|
+
hydrate: {
|
|
13
|
+
deserializeData: SuperJSON.deserialize,
|
|
14
|
+
},
|
|
15
|
+
dehydrate: {
|
|
16
|
+
serializeData: SuperJSON.serialize,
|
|
17
|
+
shouldDehydrateQuery: (query) => {
|
|
18
|
+
return defaultShouldDehydrateQuery(query) || query.state.status === 'pending'
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
})
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const getQueryClient = () => {
|
|
26
|
+
if (typeof window === 'undefined') {
|
|
27
|
+
return createQueryClient()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return (clientQueryClientSingleton ??= createQueryClient())
|
|
31
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import SuperJSON from 'superjson'
|
|
4
|
+
|
|
5
|
+
import { createTRPCReact } from '@trpc/react-query'
|
|
6
|
+
import { httpBatchLink } from '@trpc/client'
|
|
7
|
+
|
|
8
|
+
import { type ApiRouter } from '@/server/trpc'
|
|
9
|
+
import { errorLink, getBaseUrl } from './utils'
|
|
10
|
+
|
|
11
|
+
export const api = createTRPCReact<ApiRouter>()
|
|
12
|
+
|
|
13
|
+
export const createApiClient = () => {
|
|
14
|
+
return api.createClient({
|
|
15
|
+
links: [
|
|
16
|
+
errorLink,
|
|
17
|
+
httpBatchLink({
|
|
18
|
+
transformer: SuperJSON,
|
|
19
|
+
url: getBaseUrl() + '/api/trpc',
|
|
20
|
+
}),
|
|
21
|
+
],
|
|
22
|
+
})
|
|
23
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type TRPCLink } from '@trpc/client'
|
|
4
|
+
import { observable } from '@trpc/server/observable'
|
|
5
|
+
|
|
6
|
+
import { message } from '@/components'
|
|
7
|
+
import { authExemptPaths } from '@/consts'
|
|
8
|
+
import { type ApiRouter } from '@/server/trpc'
|
|
9
|
+
|
|
10
|
+
const isRN = () => {
|
|
11
|
+
return typeof navigator !== 'undefined' && navigator.product === 'ReactNative'
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const redirectToSignIn = () => {
|
|
15
|
+
if (typeof window === 'undefined') {
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const authExemptPathSet = new Set(authExemptPaths)
|
|
20
|
+
if (authExemptPathSet.has(window.location.pathname)) {
|
|
21
|
+
return
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const callbackUrl = `${window.location.pathname}${window.location.search}${window.location.hash}`
|
|
25
|
+
window.location.replace(`/auth/signin?callbackUrl=${encodeURIComponent(callbackUrl)}`)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const getBaseUrl = () => {
|
|
29
|
+
if (isRN()) {
|
|
30
|
+
if (__DEV__) {
|
|
31
|
+
return 'http://192.168.1.16:3000'
|
|
32
|
+
}
|
|
33
|
+
return 'https://app.zhenyu.pro'
|
|
34
|
+
}
|
|
35
|
+
if (typeof window !== 'undefined') {
|
|
36
|
+
return window.location.origin
|
|
37
|
+
}
|
|
38
|
+
return `http://localhost:${process.env.PORT ?? 3000}`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export const errorLink: TRPCLink<ApiRouter> = () => {
|
|
42
|
+
return ({ next, op }) => {
|
|
43
|
+
return observable(observer => {
|
|
44
|
+
const subscription = next(op).subscribe({
|
|
45
|
+
next: value => observer.next(value),
|
|
46
|
+
complete: () => observer.complete(),
|
|
47
|
+
error: err => {
|
|
48
|
+
if (isRN()) {
|
|
49
|
+
console.error(666, 'tRPC request failed:', err)
|
|
50
|
+
} else if (err.data?.code === 'UNAUTHORIZED') {
|
|
51
|
+
redirectToSignIn()
|
|
52
|
+
} else {
|
|
53
|
+
void message.error(err.data?.formattedError || err.message || '操作失败')
|
|
54
|
+
}
|
|
55
|
+
observer.error(err)
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
return () => subscription.unsubscribe()
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"esModuleInterop": true,
|
|
4
|
+
"skipLibCheck": true,
|
|
5
|
+
"target": "es2022",
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"resolveJsonModule": true,
|
|
8
|
+
"moduleDetection": "force",
|
|
9
|
+
"isolatedModules": true,
|
|
10
|
+
"strict": true,
|
|
11
|
+
"noUncheckedIndexedAccess": true,
|
|
12
|
+
"checkJs": true,
|
|
13
|
+
"lib": ["dom", "dom.iterable", "ES2022"],
|
|
14
|
+
"noEmit": true,
|
|
15
|
+
"module": "ESNext",
|
|
16
|
+
"moduleResolution": "Bundler",
|
|
17
|
+
"jsx": "react-jsx",
|
|
18
|
+
"plugins": [
|
|
19
|
+
{
|
|
20
|
+
"name": "next"
|
|
21
|
+
}
|
|
22
|
+
],
|
|
23
|
+
"incremental": true,
|
|
24
|
+
"tsBuildInfoFile": ".next/cache/tsconfig.tsbuildinfo",
|
|
25
|
+
"baseUrl": ".",
|
|
26
|
+
"paths": {
|
|
27
|
+
"@/*": ["./src/*"],
|
|
28
|
+
"@prisma/client": ["./src/server/generated/prisma-client"]
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"include": [
|
|
32
|
+
".eslintrc.cjs",
|
|
33
|
+
"next-env.d.ts",
|
|
34
|
+
"**/*.ts",
|
|
35
|
+
"**/*.tsx",
|
|
36
|
+
"**/*.cjs",
|
|
37
|
+
"**/*.js",
|
|
38
|
+
".next/types/**/*.ts",
|
|
39
|
+
".next/dev/types/**/*.ts"
|
|
40
|
+
],
|
|
41
|
+
"exclude": [
|
|
42
|
+
"node_modules",
|
|
43
|
+
"src/server/generated",
|
|
44
|
+
"**/.next"
|
|
45
|
+
]
|
|
46
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "__WZYJS_APP_NAME__",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "next dev --turbopack",
|
|
8
|
+
"build": "NODE_ENV=production next build --turbopack",
|
|
9
|
+
"start": "next start",
|
|
10
|
+
"generate": "rm -rf src/server/generated && zenstack generate --output ./src/server/generated/.zenstack --no-compile --schema ./prisma/schema.zmodel",
|
|
11
|
+
"db:push": "prisma db push",
|
|
12
|
+
"lint": "eslint .",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"check": "bun run lint && bun run typecheck",
|
|
15
|
+
"postinstall": "bun run generate"
|
|
16
|
+
},
|
|
17
|
+
"dependencies": {
|
|
18
|
+
"@auth/prisma-adapter": "^2.9.1",
|
|
19
|
+
"@prisma/client": "^6.14.0",
|
|
20
|
+
"@t3-oss/env-nextjs": "^0.10.1",
|
|
21
|
+
"@tanstack/react-query": "^5.50.0",
|
|
22
|
+
"@trpc/client": "^11.7.0",
|
|
23
|
+
"@trpc/next": "^11.7.0",
|
|
24
|
+
"@trpc/react-query": "^11.7.0",
|
|
25
|
+
"@trpc/server": "^11.7.0",
|
|
26
|
+
"@wzyjs/hooks": "workspace:*",
|
|
27
|
+
"@wzyjs/next": "workspace:*",
|
|
28
|
+
"@wzyjs/utils": "workspace:*",
|
|
29
|
+
"@wzyjs/uis": "workspace:*",
|
|
30
|
+
"@zenstackhq/runtime": "2.22.1",
|
|
31
|
+
"@zenstackhq/trpc": "2.22.1",
|
|
32
|
+
"antd": "^6.3.1",
|
|
33
|
+
"next": "^16.2.9",
|
|
34
|
+
"next-auth": "^4.24.11",
|
|
35
|
+
"react": "^19.1.0",
|
|
36
|
+
"react-dom": "^19.1.0",
|
|
37
|
+
"server-only": "^0.0.1",
|
|
38
|
+
"superjson": "^2.2.1",
|
|
39
|
+
"zenstack": "2.22.1",
|
|
40
|
+
"zod": "^3.23.3",
|
|
41
|
+
"zod-validation-error": "^3.4.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^20",
|
|
45
|
+
"@types/react": "19.1.10",
|
|
46
|
+
"@types/react-dom": "^19",
|
|
47
|
+
"eslint": "^9",
|
|
48
|
+
"eslint-config-next": "^16.2.9",
|
|
49
|
+
"postcss": "^8.4.39",
|
|
50
|
+
"prisma": "^6.14.0",
|
|
51
|
+
"prisma-json-types-generator": "^3.3.0",
|
|
52
|
+
"tailwindcss": "^3.4.3",
|
|
53
|
+
"typescript": "^5"
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import "@wzyjs/next/Base"
|
|
2
|
+
import "./User"
|
|
3
|
+
|
|
4
|
+
model AuthAccount extends Base {
|
|
5
|
+
userId String
|
|
6
|
+
type String
|
|
7
|
+
provider String
|
|
8
|
+
providerAccountId String
|
|
9
|
+
refresh_token String? @db.Text
|
|
10
|
+
access_token String? @db.Text
|
|
11
|
+
expires_at Int?
|
|
12
|
+
token_type String?
|
|
13
|
+
scope String?
|
|
14
|
+
id_token String? @db.Text
|
|
15
|
+
session_state String?
|
|
16
|
+
|
|
17
|
+
user AuthUser @relation("AuthUserToAccount", fields: [userId], references: [id], onDelete: Cascade)
|
|
18
|
+
|
|
19
|
+
@@unique([provider, providerAccountId])
|
|
20
|
+
@@deny('all', auth() == null)
|
|
21
|
+
@@deny('read', isDeleted)
|
|
22
|
+
@@deny('read', userId != auth().id)
|
|
23
|
+
@@deny('create', true)
|
|
24
|
+
@@deny('update', true)
|
|
25
|
+
@@deny('delete', true)
|
|
26
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import "@wzyjs/next/Base"
|
|
2
|
+
import "./User"
|
|
3
|
+
|
|
4
|
+
model AuthSession extends Base {
|
|
5
|
+
sessionToken String @unique
|
|
6
|
+
userId String
|
|
7
|
+
expires DateTime
|
|
8
|
+
|
|
9
|
+
user AuthUser @relation("AuthUserToSession", fields: [userId], references: [id], onDelete: Cascade)
|
|
10
|
+
|
|
11
|
+
@@deny('all', auth() == null)
|
|
12
|
+
@@deny('read', isDeleted)
|
|
13
|
+
@@deny('read', userId != auth().id)
|
|
14
|
+
@@deny('create', true)
|
|
15
|
+
@@deny('update', true)
|
|
16
|
+
@@deny('delete', true)
|
|
17
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import "@wzyjs/next/Base"
|
|
2
|
+
|
|
3
|
+
import "./Account"
|
|
4
|
+
import "./Session"
|
|
5
|
+
|
|
6
|
+
model AuthUser extends Base {
|
|
7
|
+
email String @unique @deny('update', true)
|
|
8
|
+
emailVerified DateTime?
|
|
9
|
+
name String?
|
|
10
|
+
image String?
|
|
11
|
+
|
|
12
|
+
accounts AuthAccount[] @relation("AuthUserToAccount")
|
|
13
|
+
sessions AuthSession[] @relation("AuthUserToSession")
|
|
14
|
+
|
|
15
|
+
@@auth()
|
|
16
|
+
@@deny('all', auth() == null)
|
|
17
|
+
@@deny('read', isDeleted)
|
|
18
|
+
@@deny('read', id != auth().id)
|
|
19
|
+
@@deny('update', id != auth().id)
|
|
20
|
+
@@deny('create', true)
|
|
21
|
+
@@deny('delete', true)
|
|
22
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import Link from 'next/link'
|
|
4
|
+
import { Suspense } from 'react'
|
|
5
|
+
import { useSearchParams } from 'next/navigation'
|
|
6
|
+
|
|
7
|
+
import { Button, PanelPage, Result, Spin, Typography } from '@/components'
|
|
8
|
+
|
|
9
|
+
const errorMessageMap: Record<string, string> = {
|
|
10
|
+
OAuthSignin: '无法发起 Google 登录,请稍后再试。',
|
|
11
|
+
OAuthCallback: 'Google 登录回调失败,请检查回调地址配置。',
|
|
12
|
+
OAuthCreateAccount: '创建登录账号失败,请稍后再试。',
|
|
13
|
+
EmailCreateAccount: '创建登录账号失败,请稍后再试。',
|
|
14
|
+
Callback: '登录流程中断,请重新发起登录。',
|
|
15
|
+
OAuthAccountNotLinked: '当前邮箱已绑定其他登录方式。',
|
|
16
|
+
EmailSignin: '邮件登录失败。',
|
|
17
|
+
CredentialsSignin: '账号校验失败。',
|
|
18
|
+
SessionRequired: '当前页面需要先登录。',
|
|
19
|
+
default: '登录失败,请稍后重试。',
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const AuthErrorContent = () => {
|
|
23
|
+
const searchParams = useSearchParams()
|
|
24
|
+
const error = searchParams.get('error') || 'default'
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<PanelPage eyebrow='Authentication Exception' title='登录失败'>
|
|
28
|
+
<Result
|
|
29
|
+
status='error'
|
|
30
|
+
title='本次认证没有成功完成'
|
|
31
|
+
subTitle={errorMessageMap[error] ?? errorMessageMap.default}
|
|
32
|
+
extra={[
|
|
33
|
+
<Link href='/auth/signin' key='signin'>
|
|
34
|
+
<Button type='primary' className='!rounded-lg px-6'>
|
|
35
|
+
返回登录页
|
|
36
|
+
</Button>
|
|
37
|
+
</Link>,
|
|
38
|
+
]}
|
|
39
|
+
/>
|
|
40
|
+
|
|
41
|
+
<div className='mx-4 mb-4 rounded-lg bg-white/70 p-5 dark:bg-white/5 sm:mx-6'>
|
|
42
|
+
<Typography.Paragraph className='!mb-0 text-center !text-sm !leading-6 !text-slate-500 dark:!text-slate-300'>
|
|
43
|
+
请检查 Google OAuth Client、NEXTAUTH_URL 和 NEXTAUTH_SECRET 配置。
|
|
44
|
+
</Typography.Paragraph>
|
|
45
|
+
</div>
|
|
46
|
+
</PanelPage>
|
|
47
|
+
)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export default function AuthErrorPage() {
|
|
51
|
+
return (
|
|
52
|
+
<Suspense fallback={<Spin />}>
|
|
53
|
+
<AuthErrorContent />
|
|
54
|
+
</Suspense>
|
|
55
|
+
)
|
|
56
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Suspense, useEffect } from 'react'
|
|
4
|
+
import { signIn, useSession } from 'next-auth/react'
|
|
5
|
+
import { useRouter, useSearchParams } from 'next/navigation'
|
|
6
|
+
|
|
7
|
+
import { App, Button, GoogleOutlined, PanelPage, Space, Spin, Typography } from '@/components'
|
|
8
|
+
import { getSafeCallbackPath } from '@/utils'
|
|
9
|
+
|
|
10
|
+
const { Text } = Typography
|
|
11
|
+
|
|
12
|
+
const SignInPage = () => {
|
|
13
|
+
const { message } = App.useApp()
|
|
14
|
+
const { data: session, status } = useSession()
|
|
15
|
+
const router = useRouter()
|
|
16
|
+
const searchParams = useSearchParams()
|
|
17
|
+
|
|
18
|
+
const callbackUrl = getSafeCallbackPath(searchParams.get('callbackUrl'), '/')
|
|
19
|
+
const error = searchParams.get('error')
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
if (status === 'authenticated' && session) {
|
|
23
|
+
router.push(callbackUrl)
|
|
24
|
+
}
|
|
25
|
+
}, [callbackUrl, router, session, status])
|
|
26
|
+
|
|
27
|
+
const onGoogleSignIn = async () => {
|
|
28
|
+
try {
|
|
29
|
+
await signIn('google', { callbackUrl })
|
|
30
|
+
} catch (signinError) {
|
|
31
|
+
console.error(666, '登录失败:', signinError)
|
|
32
|
+
void message.error('登录失败,请稍后重试')
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<PanelPage
|
|
38
|
+
eyebrow='Google Sign In'
|
|
39
|
+
title='账号登录'
|
|
40
|
+
description='使用 Google 账号登录后即可访问应用。'
|
|
41
|
+
>
|
|
42
|
+
<Space orientation='vertical' size='large' className='w-full'>
|
|
43
|
+
{error ? (
|
|
44
|
+
<div className='rounded-lg border border-red-200 bg-red-50/90 p-4 text-left dark:border-red-500/30 dark:bg-red-500/10'>
|
|
45
|
+
<div className='text-sm font-semibold text-red-700 dark:text-red-300'>
|
|
46
|
+
登录流程没有完成
|
|
47
|
+
</div>
|
|
48
|
+
<p className='mt-2 text-sm leading-6 text-red-600 dark:text-red-200'>
|
|
49
|
+
请重试一次;如果问题持续出现,优先检查 Google Client 配置和回调地址。
|
|
50
|
+
</p>
|
|
51
|
+
</div>
|
|
52
|
+
) : null}
|
|
53
|
+
|
|
54
|
+
<Button
|
|
55
|
+
type='primary'
|
|
56
|
+
size='large'
|
|
57
|
+
className='!h-12 !rounded-lg !text-sm !font-semibold shadow-sm'
|
|
58
|
+
icon={<GoogleOutlined />}
|
|
59
|
+
onClick={() => {
|
|
60
|
+
void onGoogleSignIn()
|
|
61
|
+
}}
|
|
62
|
+
block
|
|
63
|
+
>
|
|
64
|
+
使用 Google 登录
|
|
65
|
+
</Button>
|
|
66
|
+
|
|
67
|
+
<div className='rounded-lg bg-white/70 p-5 text-center dark:bg-white/5'>
|
|
68
|
+
<Text className='text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
|
69
|
+
首次使用前,请在 `.env` 中配置 Google OAuth Client 信息。
|
|
70
|
+
</Text>
|
|
71
|
+
</div>
|
|
72
|
+
</Space>
|
|
73
|
+
</PanelPage>
|
|
74
|
+
)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export default function SignIn() {
|
|
78
|
+
return (
|
|
79
|
+
<Suspense fallback={<Spin />}>
|
|
80
|
+
<SignInPage />
|
|
81
|
+
</Suspense>
|
|
82
|
+
)
|
|
83
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ReactNode } from 'react'
|
|
2
|
+
import { type Metadata } from 'next'
|
|
3
|
+
|
|
4
|
+
import { AuthGuard } from '@/components/base/AuthGuard'
|
|
5
|
+
import { Providers } from '@/components/base/Providers'
|
|
6
|
+
import { Layout } from '@/components/base/Layout'
|
|
7
|
+
|
|
8
|
+
import '@/styles/globals.css'
|
|
9
|
+
|
|
10
|
+
export const metadata: Metadata = {
|
|
11
|
+
title: '__WZYJS_APP_DISPLAY_NAME__',
|
|
12
|
+
icons: [{ rel: 'icon', url: '/favicon.ico' }],
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface RootLayoutProps {
|
|
16
|
+
children: ReactNode
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export default function RootLayout(props: RootLayoutProps) {
|
|
20
|
+
const { children } = props
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<html lang='zh-CN' suppressHydrationWarning>
|
|
24
|
+
<body suppressHydrationWarning>
|
|
25
|
+
<Providers>
|
|
26
|
+
<AuthGuard>
|
|
27
|
+
<Layout>
|
|
28
|
+
{children}
|
|
29
|
+
</Layout>
|
|
30
|
+
</AuthGuard>
|
|
31
|
+
</Providers>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
)
|
|
35
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { Spin, Typography } from '@/components'
|
|
4
|
+
|
|
5
|
+
export const LayoutLoading = () => {
|
|
6
|
+
return (
|
|
7
|
+
<div className='flex min-h-screen items-center justify-center bg-slate-50 p-6 text-slate-950 transition-colors dark:bg-[#14171c] dark:text-[#e8edf5]'>
|
|
8
|
+
<div className='w-full max-w-md rounded-lg border border-slate-200 bg-white p-8 text-center shadow-sm dark:border-[#303846] dark:bg-[#1b2028]'>
|
|
9
|
+
<Spin size='large' />
|
|
10
|
+
<Typography.Title level={4} className='!mb-0 !mt-6 !text-xl !font-semibold !text-slate-950 dark:!text-[#e8edf5]'>
|
|
11
|
+
正在加载
|
|
12
|
+
</Typography.Title>
|
|
13
|
+
<Typography.Text className='mt-3 text-sm leading-6 text-slate-500 dark:text-slate-300'>
|
|
14
|
+
请稍候,正在准备页面内容。
|
|
15
|
+
</Typography.Text>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
)
|
|
19
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { type ReactNode, useEffect } from 'react'
|
|
4
|
+
import { useSession } from 'next-auth/react'
|
|
5
|
+
import { usePathname, useRouter } from 'next/navigation'
|
|
6
|
+
|
|
7
|
+
import { authExemptPaths } from '@/consts'
|
|
8
|
+
|
|
9
|
+
import { LayoutLoading } from './Loading'
|
|
10
|
+
|
|
11
|
+
interface AuthGuardProps {
|
|
12
|
+
children: ReactNode
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const AuthGuard = (props: AuthGuardProps) => {
|
|
16
|
+
const { children } = props
|
|
17
|
+
|
|
18
|
+
const { status } = useSession()
|
|
19
|
+
const pathname = usePathname()
|
|
20
|
+
const router = useRouter()
|
|
21
|
+
|
|
22
|
+
const isAuthExemptPath = pathname ? authExemptPaths.includes(pathname) : false
|
|
23
|
+
|
|
24
|
+
useEffect(() => {
|
|
25
|
+
if (!pathname || status !== 'unauthenticated' || isAuthExemptPath) {
|
|
26
|
+
return
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
router.replace(`/auth/signin?callbackUrl=${encodeURIComponent(pathname)}`)
|
|
30
|
+
}, [isAuthExemptPath, pathname, router, status])
|
|
31
|
+
|
|
32
|
+
if (status === 'loading') {
|
|
33
|
+
return <LayoutLoading />
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (isAuthExemptPath) {
|
|
37
|
+
return children
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (status === 'unauthenticated') {
|
|
41
|
+
return null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return children
|
|
45
|
+
}
|