diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..8d2f688 --- /dev/null +++ b/.env.example @@ -0,0 +1,21 @@ +TIMEZONE=Asia/Singapore + +# PostgreSQL 配置 +POSTGRES_VERSION=latest + +# PostgreSQL Env +POSTGRES_DB=nice +POSTGRES_USER=nice +POSTGRES_PASSWORD=nice + +# Redis 配置 +REDIS_VERSION=7.2.4 +REDIS_PASSWORD=nice + +# MinIO 配置 +MINIO_VERSION=latest +MINIO_ACCESS_KEY=nice +MINIO_SECRET_KEY=nice123 +# Elasticsearch 配置 +ELASTIC_VERSION=9.0.1 +ELASTIC_PASSWORD=nice_elastic_password diff --git a/.gitignore b/.gitignore index af3063c..edd0df0 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ npm-debug.log* *.pem -packages/db/generated \ No newline at end of file +packages/db/generated +volumes \ No newline at end of file diff --git a/apps/backend/package.json b/apps/backend/package.json index a6050da..029adf6 100644 --- a/apps/backend/package.json +++ b/apps/backend/package.json @@ -12,6 +12,7 @@ "@types/oidc-provider": "^9.1.0", "hono": "^4.7.10", "ioredis": "5.4.1", + "jose": "^6.0.11", "minio": "7.1.3", "nanoid": "^5.1.5", "node-cron": "^4.0.7", diff --git a/apps/backend/src/index.ts b/apps/backend/src/index.ts index d5bfac3..3550582 100644 --- a/apps/backend/src/index.ts +++ b/apps/backend/src/index.ts @@ -44,6 +44,7 @@ app.get('/', (c) => { app.use('/oidc/*', async (c, next) => { // @ts-ignore await oidc.callback(c.req.raw, c.res.raw) - return c.finalize() + // return void 也可以 + return }) export default app diff --git a/apps/backend/src/oidc/config.ts b/apps/backend/src/oidc/config.ts index c91b3f9..09c71f6 100644 --- a/apps/backend/src/oidc/config.ts +++ b/apps/backend/src/oidc/config.ts @@ -1,37 +1,108 @@ import { Configuration } from 'oidc-provider'; -import { nanoid } from 'nanoid'; +import { RedisAdapter } from './redis-adapter'; +import { prisma } from '@repo/db'; + +async function getClients() { + const dbClients = await prisma.oidcClient.findMany?.(); + const dbClientList = (dbClients && dbClients.length > 0) + ? dbClients.map(c => ({ + client_id: c.clientId, + client_secret: c.clientSecret, + grant_types: JSON.parse(c.grantTypes), // string -> string[] + redirect_uris: JSON.parse(c.redirectUris), // string -> string[] + response_types: JSON.parse(c.responseTypes), // string -> string[] + scope: c.scope, + })) + : []; + + // 管理后台client,通过环境变量读取 + const defaultClient = { + client_id: process.env.OIDC_CLIENT_ID || 'admin-client', + client_secret: process.env.OIDC_CLIENT_SECRET || 'admin-secret', + grant_types: ['authorization_code', 'refresh_token'], + redirect_uris: [process.env.OIDC_REDIRECT_URI || 'http://localhost:3000/callback'], + response_types: ['code'], + scope: 'openid email profile', + }; + + // 检查是否与数据库client_id重复 + const allClients = [defaultClient, ...dbClientList.filter(c => c.client_id !== defaultClient.client_id)]; + + return allClients; +} + +const OIDC_COOKIE_KEY = process.env.OIDC_COOKIE_KEY || 'HrbEPlzByV0CcjFJhl2pjKV2iG8FgQIc'; const config: Configuration = { - clients: [ - { - client_id: 'example-client', - client_secret: 'example-secret', - grant_types: ['authorization_code', 'refresh_token'], - redirect_uris: ['http://localhost:3000/callback'], - response_types: ['code'], - scope: 'openid email profile', - }, - ], + adapter: RedisAdapter, + // 注意:clients字段现在是Promise,需在Provider初始化时await + clients: await getClients(), pkce: { - required: () => true, // 要求所有客户端使用PKCE + required: () => true, }, features: { - devInteractions: { enabled: false }, // 禁用开发交互界面 - resourceIndicators: { enabled: true }, // 启用资源指示器 - revocation: { enabled: true }, // 启用令牌撤销 - userinfo: { enabled: true }, // 启用用户信息端点 + devInteractions: { enabled: false }, + resourceIndicators: { enabled: true }, + revocation: { enabled: true }, + userinfo: { enabled: true }, + registration: { enabled: true }, }, cookies: { - keys: [nanoid()], // 用于签署和验证cookie + keys: [OIDC_COOKIE_KEY], }, jwks: { - keys: [], // 在实际环境中应该生成并保存密钥 + keys: [ + { + d: 'VEZOsY07JTFzGTqv6cC2Y32vsfChind2I_TTuvV225_-0zrSej3XLRg8iE_u0-3GSgiGi4WImmTwmEgLo4Qp3uEcxCYbt4NMJC7fwT2i3dfRZjtZ4yJwFl0SIj8TgfQ8ptwZbFZUlcHGXZIr4nL8GXyQT0CK8wy4COfmymHrrUoyfZA154ql_OsoiupSUCRcKVvZj2JHL2KILsq_sh_l7g2dqAN8D7jYfJ58MkqlknBMa2-zi5I0-1JUOwztVNml_zGrp27UbEU60RqV3GHjoqwI6m01U7K0a8Q_SQAKYGqgepbAYOA-P4_TLl5KC4-WWBZu_rVfwgSENwWNEhw8oQ', + dp: 'E1Y-SN4bQqX7kP-bNgZ_gEv-pixJ5F_EGocHKfS56jtzRqQdTurrk4jIVpI-ZITA88lWAHxjD-OaoJUh9Jupd_lwD5Si80PyVxOMI2xaGQiF0lbKJfD38Sh8frRpgelZVaK_gm834B6SLfxKdNsP04DsJqGKktODF_fZeaGFPH0', + dq: 'F90JPxevQYOlAgEH0TUt1-3_hyxY6cfPRU2HQBaahyWrtCWpaOzenKZnvGFZdg-BuLVKjCchq3G_70OLE-XDP_ol0UTJmDTT-WyuJQdEMpt_WFF9yJGoeIu8yohfeLatU-67ukjghJ0s9CBzNE_LrGEV6Cup3FXywpSYZAV3iqc', + e: 'AQAB', + kty: 'RSA', + n: 'xwQ72P9z9OYshiQ-ntDYaPnnfwG6u9JAdLMZ5o0dmjlcyrvwQRdoFIKPnO65Q8mh6F_LDSxjxa2Yzo_wdjhbPZLjfUJXgCzm54cClXzT5twzo7lzoAfaJlkTsoZc2HFWqmcri0BuzmTFLZx2Q7wYBm0pXHmQKF0V-C1O6NWfd4mfBhbM-I1tHYSpAMgarSm22WDMDx-WWI7TEzy2QhaBVaENW9BKaKkJklocAZCxk18WhR0fckIGiWiSM5FcU1PY2jfGsTmX505Ub7P5Dz75Ygqrutd5tFrcqyPAtPTFDk8X1InxkkUwpP3nFU5o50DGhwQolGYKPGtQ-ZtmbOfcWQ', + p: '5wC6nY6Ev5FqcLPCqn9fC6R9KUuBej6NaAVOKW7GXiOJAq2WrileGKfMc9kIny20zW3uWkRLm-O-3Yzze1zFpxmqvsvCxZ5ERVZ6leiNXSu3tez71ZZwp0O9gys4knjrI-9w46l_vFuRtjL6XEeFfHEZFaNJpz-lcnb3w0okrbM', + q: '3I1qeEDslZFB8iNfpKAdWtz_Wzm6-jayT_V6aIvhvMj5mnU-Xpj75zLPQSGa9wunMlOoZW9w1wDO1FVuDhwzeOJaTm-Ds0MezeC4U6nVGyyDHb4CUA3ml2tzt4yLrqGYMT7XbADSvuWYADHw79OFjEi4T3s3tJymhaBvy1ulv8M', + qi: 'wSbXte9PcPtr788e713KHQ4waE26CzoXx-JNOgN0iqJMN6C4_XJEX-cSvCZDf4rh7xpXN6SGLVd5ibIyDJi7bbi5EQ5AXjazPbLBjRthcGXsIuZ3AtQyR0CEWNSdM7EyM5TRdyZQ9kftfz9nI03guW3iKKASETqX2vh0Z8XRjyU', + use: 'sig', + }, { + crv: 'P-256', + d: 'K9xfPv773dZR22TVUB80xouzdF7qCg5cWjPjkHyv7Ws', + kty: 'EC', + use: 'sig', + x: 'FWZ9rSkLt6Dx9E3pxLybhdM6xgR5obGsj5_pqmnz5J4', + y: '_n8G69C-A2Xl4xUW2lF0i8ZGZnk_KPYrhv4GbTGu5G4', + }, + ], }, ttl: { - AccessToken: 3600, // 1小时 - AuthorizationCode: 600, // 10分钟 - IdToken: 3600, // 1小时 - RefreshToken: 1209600, // 14天 + AccessToken: 3600, + AuthorizationCode: 600, + IdToken: 3600, + RefreshToken: 1209600, + BackchannelAuthenticationRequest: 600, + ClientCredentials: 600, + DeviceCode: 600, + Grant: 1209600, + Interaction: 3600, + Session: 1209600, + RegistrationAccessToken: 3600, + DPoPProof: 300, + PushedAuthorizationRequest: 600, + ReplayDetection: 3600, + LogoutToken: 600, + }, + findAccount: async (ctx, id) => { + const user = await prisma.user.findUnique({ where: { id } }); + if (!user) return undefined; + return { + accountId: user.id, + async claims() { + return { + sub: user.id, + email: user.email, + name: user.name, + }; + }, + }; }, }; diff --git a/apps/backend/src/oidc/provider.ts b/apps/backend/src/oidc/provider.ts index cc725b9..aca144d 100644 --- a/apps/backend/src/oidc/provider.ts +++ b/apps/backend/src/oidc/provider.ts @@ -1,6 +1,6 @@ import { Provider } from 'oidc-provider'; import config from './config'; -const oidc = new Provider('http://localhost:4000', config); +const oidc = new Provider('http://localhost:3000', config); export default oidc; \ No newline at end of file diff --git a/apps/backend/src/oidc/redis-adapter.ts b/apps/backend/src/oidc/redis-adapter.ts new file mode 100644 index 0000000..d62fe20 --- /dev/null +++ b/apps/backend/src/oidc/redis-adapter.ts @@ -0,0 +1,86 @@ +import type { Adapter, AdapterPayload } from 'oidc-provider'; +import redis from '../redis'; + +export class RedisAdapter implements Adapter { + name: string; + constructor(name: string) { + this.name = name; + } + + key(id: string) { + return `${this.name}:${id}`; + } + + async upsert(id: string, payload: AdapterPayload, expiresIn: number) { + const key = this.key(id); + await redis.set(key, JSON.stringify(payload), 'EX', expiresIn); + if (payload && payload.grantId) { + // 记录grantId到id的映射,便于revokeByGrantId + await redis.sadd(this.grantKey(payload.grantId), id); + await redis.expire(this.grantKey(payload.grantId), expiresIn); + } + if (payload && payload.userCode) { + await redis.set(this.userCodeKey(payload.userCode), id, 'EX', expiresIn); + } + if (payload && payload.uid) { + await redis.set(this.uidKey(payload.uid), id, 'EX', expiresIn); + } + } + + async find(id: string) { + const data = await redis.get(this.key(id)); + return data ? JSON.parse(data) : undefined; + } + + async findByUserCode(userCode: string) { + const id = await redis.get(this.userCodeKey(userCode)); + return id ? this.find(id) : undefined; + } + + async findByUid(uid: string) { + const id = await redis.get(this.uidKey(uid)); + return id ? this.find(id) : undefined; + } + + async destroy(id: string) { + const data = await this.find(id); + await redis.del(this.key(id)); + if (data && data.grantId) { + await redis.srem(this.grantKey(data.grantId), id); + } + if (data && data.userCode) { + await redis.del(this.userCodeKey(data.userCode)); + } + if (data && data.uid) { + await redis.del(this.uidKey(data.uid)); + } + } + + async revokeByGrantId(grantId: string) { + const key = this.grantKey(grantId); + const ids = await redis.smembers(key); + if (ids && ids.length) { + await Promise.all(ids.map((id) => this.destroy(id))); + } + await redis.del(key); + } + + async consume(id: string) { + const key = this.key(id); + const data = await this.find(id); + if (data) { + data.consumed = Math.floor(Date.now() / 1000); + await redis.set(key, JSON.stringify(data)); + } + } + + grantKey(grantId: string) { + return `${this.name}:grant:${grantId}`; + } + userCodeKey(userCode: string) { + return `${this.name}:userCode:${userCode}`; + } + uidKey(uid: string) { + return `${this.name}:uid:${uid}`; + } +} diff --git a/apps/backend/src/redis.ts b/apps/backend/src/redis.ts index b2aebe3..494dc10 100644 --- a/apps/backend/src/redis.ts +++ b/apps/backend/src/redis.ts @@ -4,7 +4,7 @@ import Redis from 'ioredis'; const redis = new Redis({ host: 'localhost', // 根据实际情况配置 port: 6379, - // password: 'yourpassword', // 如有需要 + password: process.env.REDIS_PASSWORD }); export default redis; \ No newline at end of file diff --git a/apps/backend/src/trpc.ts b/apps/backend/src/trpc.ts index db54440..9e751a2 100644 --- a/apps/backend/src/trpc.ts +++ b/apps/backend/src/trpc.ts @@ -1,5 +1,6 @@ import { z } from 'zod' import { initTRPC } from '@trpc/server' +import { userRouter } from './user/user.trpc' const t = initTRPC.create() @@ -7,9 +8,7 @@ export const publicProcedure = t.procedure export const router = t.router export const appRouter = router({ - hello: publicProcedure.input(z.string().nullish()).query(({ input }) => { - return `Hello ${input ?? 'World'}!` - }), + user: userRouter }) export type AppRouter = typeof appRouter \ No newline at end of file diff --git a/apps/backend/src/user/userindex.ts b/apps/backend/src/user/user.index.ts similarity index 100% rename from apps/backend/src/user/userindex.ts rename to apps/backend/src/user/user.index.ts diff --git a/apps/backend/src/user/routes.ts b/apps/backend/src/user/user.rest.ts similarity index 57% rename from apps/backend/src/user/routes.ts rename to apps/backend/src/user/user.rest.ts index 51031a0..7cfb3ad 100644 --- a/apps/backend/src/user/routes.ts +++ b/apps/backend/src/user/user.rest.ts @@ -1,17 +1,17 @@ import { Hono } from "hono"; -import { createUser, searchUser } from "./userindex"; +import { createUser, searchUser } from "./user.index"; -const userRoute = new Hono(); +const userRest = new Hono(); -userRoute.post('/', async (c) => { +userRest.post('/', async (c) => { const user = await c.req.json(); const result = await createUser(user); return c.json(result); }); -userRoute.get('/search', async (c) => { +userRest.get('/search', async (c) => { const q = c.req.query('q') || ''; const result = await searchUser(q); return c.json(result.hits.hits); }); -export default userRoute; \ No newline at end of file +export default userRest; \ No newline at end of file diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index c9841e8..b76f59b 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -1,46 +1,47 @@ 'use client'; -import { api } from '@repo/client'; -import { Button } from '@repo/ui/components/button'; -import { useState } from 'react'; -import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; -import { httpBatchLink } from '@trpc/client'; +// import { api } from '@repo/client'; +// import { Button } from '@repo/ui/components/button'; +// import { useState } from 'react'; +// import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +// import { httpBatchLink } from '@trpc/client'; -function HomeContent() { - const [name, setName] = useState(''); - const helloQuery = api.hello.useQuery(name || undefined); +// function HomeContent() { +// const [name, setName] = useState(''); +// const helloQuery = api.hello.useQuery(name || undefined); - return ( -
- setName(e.target.value)} - placeholder="输入名字" - className="border p-2 mr-2" - /> - -
- ); -} +// return ( +//
+// setName(e.target.value)} +// placeholder="输入名字" +// className="border p-2 mr-2" +// /> +// +//
+// ); +// } export default function Home() { - const [queryClient] = useState(() => new QueryClient()); - const [trpcClient] = useState(() => - api.createClient({ - links: [ - httpBatchLink({ - url: 'http://localhost:3000/api/trpc', - }), - ], - }), - ); + // const [queryClient] = useState(() => new QueryClient()); + // const [trpcClient] = useState(() => + // api.createClient({ + // links: [ + // httpBatchLink({ + // url: 'http://localhost:3000/api/trpc', + // }), + // ], + // }), + // ); return ( - - - - - +
123
+ // + // + // + // + // ); } diff --git a/apps/web/providers/query-provider.tsx b/apps/web/providers/query-provider.tsx index ca5441a..9f0f2c1 100644 --- a/apps/web/providers/query-provider.tsx +++ b/apps/web/providers/query-provider.tsx @@ -10,7 +10,7 @@ export default function QueryProvider({ children }) { const accessToken = ''; // 使用Next.js环境变量 - const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'; + const apiUrl = process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3000'; // Set the default query options including staleTime. const [queryClient] = useState( diff --git a/packages/db/prisma/migrations/20250526141831_add_oidc_client/migration.sql b/packages/db/prisma/migrations/20250526141831_add_oidc_client/migration.sql new file mode 100644 index 0000000..ace7e67 --- /dev/null +++ b/packages/db/prisma/migrations/20250526141831_add_oidc_client/migration.sql @@ -0,0 +1,95 @@ +/* + Warnings: + + - You are about to drop the column `active` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `client_id` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `client_name` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `client_secret` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `client_uri` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `contacts` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `created_by` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `created_time` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `grant_types` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `jwks` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `jwks_uri` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `last_modified_time` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `logo_uri` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `policy_uri` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `post_logout_redirect_uris` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `redirect_uris` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `require_pkce` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `response_types` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `token_endpoint_auth_method` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the column `tos_uri` on the `oidc_clients` table. All the data in the column will be lost. + - You are about to drop the `oidc_authorization_codes` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `oidc_consents` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `oidc_key_pairs` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `oidc_sessions` table. If the table is not empty, all the data it contains will be lost. + - You are about to drop the `oidc_tokens` table. If the table is not empty, all the data it contains will be lost. + - A unique constraint covering the columns `[clientId]` on the table `oidc_clients` will be added. If there are existing duplicate values, this will fail. + - Added the required column `clientId` to the `oidc_clients` table without a default value. This is not possible if the table is not empty. + - Added the required column `clientSecret` to the `oidc_clients` table without a default value. This is not possible if the table is not empty. + - Added the required column `grantTypes` to the `oidc_clients` table without a default value. This is not possible if the table is not empty. + - Added the required column `redirectUris` to the `oidc_clients` table without a default value. This is not possible if the table is not empty. + - Added the required column `responseTypes` to the `oidc_clients` table without a default value. This is not possible if the table is not empty. + - Added the required column `updatedAt` to the `oidc_clients` table without a default value. This is not possible if the table is not empty. + +*/ +-- DropForeignKey +ALTER TABLE "oidc_authorization_codes" DROP CONSTRAINT "oidc_authorization_codes_client_id_fkey"; + +-- DropForeignKey +ALTER TABLE "oidc_consents" DROP CONSTRAINT "oidc_consents_client_id_fkey"; + +-- DropForeignKey +ALTER TABLE "oidc_tokens" DROP CONSTRAINT "oidc_tokens_client_id_fkey"; + +-- DropIndex +DROP INDEX "oidc_clients_client_id_key"; + +-- AlterTable +ALTER TABLE "oidc_clients" DROP COLUMN "active", +DROP COLUMN "client_id", +DROP COLUMN "client_name", +DROP COLUMN "client_secret", +DROP COLUMN "client_uri", +DROP COLUMN "contacts", +DROP COLUMN "created_by", +DROP COLUMN "created_time", +DROP COLUMN "grant_types", +DROP COLUMN "jwks", +DROP COLUMN "jwks_uri", +DROP COLUMN "last_modified_time", +DROP COLUMN "logo_uri", +DROP COLUMN "policy_uri", +DROP COLUMN "post_logout_redirect_uris", +DROP COLUMN "redirect_uris", +DROP COLUMN "require_pkce", +DROP COLUMN "response_types", +DROP COLUMN "token_endpoint_auth_method", +DROP COLUMN "tos_uri", +ADD COLUMN "clientId" TEXT NOT NULL, +ADD COLUMN "clientSecret" TEXT NOT NULL, +ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, +ADD COLUMN "grantTypes" TEXT NOT NULL, +ADD COLUMN "redirectUris" TEXT NOT NULL, +ADD COLUMN "responseTypes" TEXT NOT NULL, +ADD COLUMN "updatedAt" TIMESTAMP(3) NOT NULL; + +-- DropTable +DROP TABLE "oidc_authorization_codes"; + +-- DropTable +DROP TABLE "oidc_consents"; + +-- DropTable +DROP TABLE "oidc_key_pairs"; + +-- DropTable +DROP TABLE "oidc_sessions"; + +-- DropTable +DROP TABLE "oidc_tokens"; + +-- CreateIndex +CREATE UNIQUE INDEX "oidc_clients_clientId_key" ON "oidc_clients"("clientId"); diff --git a/packages/db/prisma/migrations/migration_lock.toml b/packages/db/prisma/migrations/migration_lock.toml index fbffa92..044d57c 100644 --- a/packages/db/prisma/migrations/migration_lock.toml +++ b/packages/db/prisma/migrations/migration_lock.toml @@ -1,3 +1,3 @@ # Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file +# It should be added in your version-control system (e.g., Git) +provider = "postgresql" diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index fb66da0..6c5b6c2 100755 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -99,120 +99,16 @@ model UserLastVisit { @@map("user_last_visit") } -// OIDC 客户端相关模型 model OidcClient { - id String @id @default(cuid()) - clientId String @unique @map("client_id") - clientSecret String? @map("client_secret") - clientName String @map("client_name") - clientUri String? @map("client_uri") - logoUri String? @map("logo_uri") - contacts String[] - redirectUris String[] @map("redirect_uris") - postLogoutRedirectUris String[] @map("post_logout_redirect_uris") - tokenEndpointAuthMethod String @map("token_endpoint_auth_method") - grantTypes String[] @map("grant_types") - responseTypes String[] @map("response_types") - scope String - jwksUri String? @map("jwks_uri") - jwks String? - policyUri String? @map("policy_uri") - tosUri String? @map("tos_uri") - requirePkce Boolean @default(false) @map("require_pkce") - active Boolean @default(true) - createdBy String? @map("created_by") - createdTime DateTime @default(now()) @map("created_time") - lastModifiedTime DateTime? @updatedAt @map("last_modified_time") - - // 关联模型 - consents OidcConsent[] - authorizationCodes OidcCode[] - tokens OidcToken[] + id String @id @default(cuid()) + clientId String @unique + clientSecret String + redirectUris String // 存储为JSON字符串 + grantTypes String // 存储为JSON字符串 + responseTypes String // 存储为JSON字符串 + scope String + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt @@map("oidc_clients") } - -// 用户同意记录 -model OidcConsent { - id String @id @default(cuid()) - userId String @map("user_id") - clientId String @map("client_id") - scope String - createdTime DateTime @default(now()) @map("created_time") - expiresAt DateTime? @map("expires_at") - - // 关联 - client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade) - - @@unique([userId, clientId]) - @@map("oidc_consents") -} - -// 授权码 -model OidcCode { - id String @id @default(cuid()) - code String @unique - userId String @map("user_id") - clientId String @map("client_id") - scope String - redirectUri String @map("redirect_uri") - codeChallenge String? @map("code_challenge") - codeChallengeMethod String? @map("code_challenge_method") - nonce String? - authTime DateTime @default(now()) @map("auth_time") - expiresAt DateTime @map("expires_at") - used Boolean @default(false) - - // 关联 - client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade) - - @@map("oidc_authorization_codes") -} - -// 统一令牌表(合并access和refresh token) -model OidcToken { - id String @id @default(cuid()) - token String @unique - userId String @map("user_id") - clientId String @map("client_id") - tokenType String @map("token_type") // "access" 或 "refresh" - scope String - expiresAt DateTime @map("expires_at") - createdTime DateTime @default(now()) @map("created_time") - isRevoked Boolean @default(false) @map("is_revoked") - parentId String? @map("parent_id") // 用于关联refresh token和对应的access token - - // 关联 - client OidcClient @relation(fields: [clientId], references: [id], onDelete: Cascade) - - @@index([userId, tokenType, isRevoked]) - @@map("oidc_tokens") -} - -// Session管理 -model OidcSession { - id String @id @default(cuid()) - sessionId String @unique @map("session_id") - userId String @map("user_id") - expiresAt DateTime @map("expires_at") - lastActive DateTime @default(now()) @map("last_active") - deviceInfo String? @map("device_info") - createdTime DateTime @default(now()) @map("created_time") - lastModifiedTime DateTime? @updatedAt @map("last_modified_time") - - @@map("oidc_sessions") -} - -// 供应商的密钥对 -model OidcKeyPair { - id String @id @default(cuid()) - kid String @unique - privateKey String @map("private_key") - publicKey String @map("public_key") - algorithm String - active Boolean @default(true) - createdTime DateTime @default(now()) @map("created_time") - expiresAt DateTime? @map("expires_at") - - @@map("oidc_key_pairs") -} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 67c076e..523e9dd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,6 +53,9 @@ importers: ioredis: specifier: 5.4.1 version: 5.4.1 + jose: + specifier: ^6.0.11 + version: 6.0.11 minio: specifier: 7.1.3 version: 7.1.3