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/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..9d857b7 100644 --- a/apps/backend/src/oidc/config.ts +++ b/apps/backend/src/oidc/config.ts @@ -1,7 +1,9 @@ import { Configuration } from 'oidc-provider'; import { nanoid } from 'nanoid'; +import { RedisAdapter } from './redis-adapter'; const config: Configuration = { + adapter: RedisAdapter, clients: [ { client_id: 'example-client', 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/packages/client/src/api/hooks/index.ts b/packages/client/src/api/hooks/index.ts index 65ddfc5..525d373 100755 --- a/packages/client/src/api/hooks/index.ts +++ b/packages/client/src/api/hooks/index.ts @@ -1,2 +1 @@ - export * from "./useEntity" diff --git a/packages/db/prisma/schema.prisma b/packages/db/prisma/schema.prisma index fb66da0..309f64b 100755 --- a/packages/db/prisma/schema.prisma +++ b/packages/db/prisma/schema.prisma @@ -98,121 +98,3 @@ model UserLastVisit { @@index([userId, resourceType]) @@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[] - - @@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") -}