This commit is contained in:
ditiqi 2025-05-27 08:20:38 +08:00
commit 54c8fa6bf2
17 changed files with 361 additions and 186 deletions

21
.env.example Normal file
View File

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

3
.gitignore vendored
View File

@ -36,4 +36,5 @@ npm-debug.log*
*.pem
packages/db/generated
packages/db/generated
volumes

View File

@ -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",

View File

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

View File

@ -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,
};
},
};
},
};

View File

@ -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;

View File

@ -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}`;
}
}

View File

@ -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;

View File

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

View File

@ -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;
export default userRest;

View File

@ -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 (
<div className="p-4">
<input
type="text"
value={name}
onChange={(e) => setName(e.target.value)}
placeholder="输入名字"
className="border p-2 mr-2"
/>
<Button onClick={() => helloQuery.refetch()}>{helloQuery.isLoading ? '加载中...' : helloQuery.data}</Button>
</div>
);
}
// return (
// <div className="p-4">
// <input
// type="text"
// value={name}
// onChange={(e) => setName(e.target.value)}
// placeholder="输入名字"
// className="border p-2 mr-2"
// />
// <Button onClick={() => helloQuery.refetch()}>{helloQuery.isLoading ? '加载中...' : helloQuery.data}</Button>
// </div>
// );
// }
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 (
<QueryClientProvider client={queryClient}>
<api.Provider client={trpcClient} queryClient={queryClient}>
<HomeContent />
</api.Provider>
</QueryClientProvider>
<div>123</div>
// <QueryClientProvider client={queryClient}>
// <api.Provider client={trpcClient} queryClient={queryClient}>
// <HomeContent />
// </api.Provider>
// </QueryClientProvider>
);
}

View File

@ -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(

View File

@ -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");

View File

@ -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"
# It should be added in your version-control system (e.g., Git)
provider = "postgresql"

View File

@ -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")
}

View File

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