mirror of
https://github.com/iflytek/skillhub.git
synced 2026-08-27 11:14:59 +00:00
fix(phase1): close auth and frontend acceptance gaps
This commit is contained in:
parent
46bda26f9e
commit
0b1607630f
18 changed files with 737 additions and 207 deletions
17
Makefile
17
Makefile
|
|
@ -1,11 +1,15 @@
|
|||
.PHONY: dev dev-down build test clean
|
||||
.PHONY: dev dev-down build test clean web-install dev-server dev-web build-web test-web typecheck-web lint-web generate-api prod-up prod-down
|
||||
|
||||
# 启动本地开发环境(仅依赖服务)
|
||||
dev:
|
||||
docker compose up -d
|
||||
@echo "Waiting for services to be healthy..."
|
||||
@sleep 5
|
||||
@echo "Services ready. Start backend with: cd server && ./mvnw spring-boot:run -Dspring-boot.run.profiles=local"
|
||||
@echo "Services ready. Start backend with: make dev-server"
|
||||
@echo "Start frontend with: make dev-web"
|
||||
|
||||
dev-server:
|
||||
cd server && ./mvnw spring-boot:run -Dspring-boot.run.profiles=local
|
||||
|
||||
# 停止本地开发环境
|
||||
dev-down:
|
||||
|
|
@ -29,6 +33,9 @@ generate-api:
|
|||
@echo "Generating OpenAPI types..."
|
||||
cd web && pnpm run generate-api
|
||||
|
||||
web-install:
|
||||
cd web && pnpm install
|
||||
|
||||
# 前端开发服务器
|
||||
dev-web:
|
||||
cd web && pnpm run dev
|
||||
|
|
@ -48,3 +55,9 @@ typecheck-web:
|
|||
# 前端代码检查
|
||||
lint-web:
|
||||
cd web && pnpm run lint
|
||||
|
||||
prod-up:
|
||||
docker compose -f docker-compose.prod.yml up -d --build
|
||||
|
||||
prod-down:
|
||||
docker compose -f docker-compose.prod.yml down
|
||||
|
|
|
|||
86
docker-compose.prod.yml
Normal file
86
docker-compose.prod.yml
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
ports:
|
||||
- "5432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: skillhub
|
||||
POSTGRES_USER: skillhub
|
||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-skillhub_prod}
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U skillhub"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
minio:
|
||||
image: minio/minio:latest
|
||||
ports:
|
||||
- "9000:9000"
|
||||
- "9001:9001"
|
||||
environment:
|
||||
MINIO_ROOT_USER: ${MINIO_ROOT_USER:-minioadmin}
|
||||
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:-minioadmin}
|
||||
command: server /data --console-address ":9001"
|
||||
volumes:
|
||||
- minio_data:/data
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
server:
|
||||
build:
|
||||
context: ./server
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "8080:8080"
|
||||
environment:
|
||||
SPRING_PROFILES_ACTIVE: prod
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://postgres:5432/skillhub
|
||||
SPRING_DATASOURCE_USERNAME: skillhub
|
||||
SPRING_DATASOURCE_PASSWORD: ${DB_PASSWORD:-skillhub_prod}
|
||||
SPRING_DATA_REDIS_HOST: redis
|
||||
SPRING_DATA_REDIS_PORT: 6379
|
||||
OAUTH2_GITHUB_CLIENT_ID: ${OAUTH2_GITHUB_CLIENT_ID}
|
||||
OAUTH2_GITHUB_CLIENT_SECRET: ${OAUTH2_GITHUB_CLIENT_SECRET}
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
redis:
|
||||
condition: service_healthy
|
||||
minio:
|
||||
condition: service_healthy
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-qO-", "http://localhost:8080/actuator/health"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 10
|
||||
start_period: 30s
|
||||
|
||||
web:
|
||||
build:
|
||||
context: ./web
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "80:80"
|
||||
depends_on:
|
||||
server:
|
||||
condition: service_healthy
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
minio_data:
|
||||
14
server/Dockerfile
Normal file
14
server/Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
FROM eclipse-temurin:21-jdk-alpine AS build
|
||||
WORKDIR /app
|
||||
COPY . .
|
||||
RUN ./mvnw package -DskipTests -B
|
||||
|
||||
FROM eclipse-temurin:21-jre-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/skillhub-app/target/*.jar app.jar
|
||||
RUN addgroup -S app && adduser -S app -G app
|
||||
USER app
|
||||
EXPOSE 8080
|
||||
HEALTHCHECK --interval=10s --timeout=3s \
|
||||
CMD wget -qO- http://localhost:8080/actuator/health || exit 1
|
||||
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-jar", "app.jar"]
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
package com.iflytek.skillhub.controller;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.annotation.AuthenticationPrincipal;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/v1/cli")
|
||||
public class CliController {
|
||||
|
||||
@GetMapping("/whoami")
|
||||
public ResponseEntity<Map<String, Object>> whoami(@AuthenticationPrincipal PlatformPrincipal principal) {
|
||||
if (principal == null) {
|
||||
return ResponseEntity.status(401).build();
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"data", Map.of(
|
||||
"userId", principal.userId(),
|
||||
"displayName", principal.displayName(),
|
||||
"email", principal.email() != null ? principal.email() : "",
|
||||
"avatarUrl", principal.avatarUrl() != null ? principal.avatarUrl() : "",
|
||||
"authType", principal.oauthProvider(),
|
||||
"platformRoles", principal.platformRoles()
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
@ -29,10 +29,14 @@ public class TokenController {
|
|||
|
||||
var result = apiTokenService.createToken(principal.userId(), name, scopeJson);
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"token", result.rawToken(),
|
||||
"id", result.entity().getId(),
|
||||
"name", result.entity().getName(),
|
||||
"tokenPrefix", result.entity().getTokenPrefix()
|
||||
"data", Map.of(
|
||||
"token", result.rawToken(),
|
||||
"id", result.entity().getId(),
|
||||
"name", result.entity().getName(),
|
||||
"tokenPrefix", result.entity().getTokenPrefix(),
|
||||
"createdAt", result.entity().getCreatedAt().toString(),
|
||||
"expiresAt", result.entity().getExpiresAt() != null ? result.entity().getExpiresAt().toString() : ""
|
||||
)
|
||||
));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,59 @@
|
|||
package com.iflytek.skillhub.controller;
|
||||
|
||||
import com.iflytek.skillhub.auth.rbac.PlatformPrincipal;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.authority.SimpleGrantedAuthority;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.authentication;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
@SpringBootTest
|
||||
@AutoConfigureMockMvc
|
||||
@ActiveProfiles("test")
|
||||
class CliControllerTest {
|
||||
|
||||
@Autowired
|
||||
private MockMvc mockMvc;
|
||||
|
||||
@Test
|
||||
void whoamiShouldReturnUnauthorizedForAnonymousRequest() throws Exception {
|
||||
mockMvc.perform(get("/api/v1/cli/whoami"))
|
||||
.andExpect(status().isUnauthorized());
|
||||
}
|
||||
|
||||
@Test
|
||||
void whoamiShouldReturnCurrentPrincipal() throws Exception {
|
||||
PlatformPrincipal principal = new PlatformPrincipal(
|
||||
7L,
|
||||
"cli-user",
|
||||
"cli@example.com",
|
||||
"",
|
||||
"api_token",
|
||||
Set.of("SKILL_ADMIN")
|
||||
);
|
||||
|
||||
var auth = new UsernamePasswordAuthenticationToken(
|
||||
principal,
|
||||
null,
|
||||
List.of(new SimpleGrantedAuthority("ROLE_SKILL_ADMIN"))
|
||||
);
|
||||
|
||||
mockMvc.perform(get("/api/v1/cli/whoami").with(authentication(auth)))
|
||||
.andExpect(status().isOk())
|
||||
.andExpect(jsonPath("$.data.userId").value(7))
|
||||
.andExpect(jsonPath("$.data.displayName").value("cli-user"))
|
||||
.andExpect(jsonPath("$.data.authType").value("api_token"))
|
||||
.andExpect(jsonPath("$.data.platformRoles[0]").value("SKILL_ADMIN"));
|
||||
}
|
||||
}
|
||||
|
|
@ -16,7 +16,6 @@ import org.springframework.stereotype.Component;
|
|||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
|
|
@ -64,4 +63,12 @@ public class ApiTokenAuthenticationFilter extends OncePerRequestFilter {
|
|||
}
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldNotFilter(HttpServletRequest request) {
|
||||
String path = request.getRequestURI();
|
||||
return !(path.startsWith("/api/v1/cli/")
|
||||
|| path.startsWith("/api/v1/tokens")
|
||||
|| path.startsWith("/api/compat/"));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
14
web/Dockerfile
Normal file
14
web/Dockerfile
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
FROM node:22-alpine AS build
|
||||
RUN corepack enable
|
||||
WORKDIR /app
|
||||
COPY package.json pnpm-lock.yaml ./
|
||||
RUN pnpm install --frozen-lockfile
|
||||
COPY . .
|
||||
RUN pnpm build
|
||||
|
||||
FROM nginx:alpine
|
||||
COPY --from=build /app/dist /usr/share/nginx/html
|
||||
COPY nginx.conf /etc/nginx/conf.d/default.conf
|
||||
EXPOSE 80
|
||||
HEALTHCHECK --interval=10s --timeout=3s \
|
||||
CMD wget -qO- http://localhost/nginx-health || exit 1
|
||||
48
web/nginx.conf
Normal file
48
web/nginx.conf
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
server {
|
||||
listen 80;
|
||||
server_name _;
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
gzip on;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml;
|
||||
gzip_min_length 1000;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /api/ {
|
||||
proxy_pass http://server:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /oauth2/ {
|
||||
proxy_pass http://server:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /login/oauth2/ {
|
||||
proxy_pass http://server:8080;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
|
||||
location /.well-known/ {
|
||||
proxy_pass http://server:8080;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
expires 1y;
|
||||
add_header Cache-Control "public, immutable";
|
||||
}
|
||||
|
||||
location /nginx-health {
|
||||
return 200 'ok';
|
||||
add_header Content-Type text/plain;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,17 +4,20 @@
|
|||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"install:ci": "pnpm install --frozen-lockfile",
|
||||
"dev": "vite",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
|
||||
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
|
||||
"generate-api": "openapi-typescript http://localhost:8080/v3/api-docs -o src/api/generated/schema.d.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"react": "^19.0.0",
|
||||
"react-dom": "^19.0.0",
|
||||
"@tanstack/react-router": "^1.95.0",
|
||||
"@tanstack/react-query": "^5.64.0",
|
||||
"openapi-fetch": "^0.13.8",
|
||||
"class-variance-authority": "^0.7.0",
|
||||
"clsx": "^2.1.0",
|
||||
"lucide-react": "^0.344.0",
|
||||
|
|
@ -33,6 +36,7 @@
|
|||
"@typescript-eslint/parser": "^7.0.0",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react-hooks": "^4.6.0",
|
||||
"eslint-plugin-react-refresh": "^0.4.5"
|
||||
"eslint-plugin-react-refresh": "^0.4.5",
|
||||
"openapi-typescript": "^7.6.1"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
195
web/pnpm-lock.yaml
generated
195
web/pnpm-lock.yaml
generated
|
|
@ -23,6 +23,9 @@ importers:
|
|||
lucide-react:
|
||||
specifier: ^0.344.0
|
||||
version: 0.344.0(react@19.2.4)
|
||||
openapi-fetch:
|
||||
specifier: ^0.13.8
|
||||
version: 0.13.8
|
||||
react:
|
||||
specifier: ^19.0.0
|
||||
version: 19.2.4
|
||||
|
|
@ -60,6 +63,9 @@ importers:
|
|||
eslint-plugin-react-refresh:
|
||||
specifier: ^0.4.5
|
||||
version: 0.4.26(eslint@8.57.1)
|
||||
openapi-typescript:
|
||||
specifier: ^7.6.1
|
||||
version: 7.13.0(typescript@5.9.3)
|
||||
postcss:
|
||||
specifier: ^8.4.0
|
||||
version: 8.5.8
|
||||
|
|
@ -377,6 +383,16 @@ packages:
|
|||
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
resolution: {integrity: sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==}
|
||||
|
||||
'@redocly/config@0.22.0':
|
||||
resolution: {integrity: sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==}
|
||||
|
||||
'@redocly/openapi-core@1.34.10':
|
||||
resolution: {integrity: sha512-XCBR/9WHJ0cpezuunHMZjuFMl4KqUo7eiFwzrQrvm7lTXt0EBd3No8UY+9OyzXpDfreGEMMtxmaLZ+ksVw378g==}
|
||||
engines: {node: '>=18.17.0', npm: '>=9.5.0'}
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27':
|
||||
resolution: {integrity: sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==}
|
||||
|
||||
|
|
@ -650,9 +666,17 @@ packages:
|
|||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
agent-base@7.1.4:
|
||||
resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ajv@6.14.0:
|
||||
resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==}
|
||||
|
||||
ansi-colors@4.1.3:
|
||||
resolution: {integrity: sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
ansi-regex@5.0.1:
|
||||
resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -727,6 +751,9 @@ packages:
|
|||
resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
change-case@5.4.4:
|
||||
resolution: {integrity: sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==}
|
||||
|
||||
chokidar@3.6.0:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
|
@ -745,6 +772,9 @@ packages:
|
|||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
colorette@1.4.0:
|
||||
resolution: {integrity: sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==}
|
||||
|
||||
commander@4.1.1:
|
||||
resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
|
@ -950,6 +980,10 @@ packages:
|
|||
resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
https-proxy-agent@7.0.6:
|
||||
resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==}
|
||||
engines: {node: '>= 14'}
|
||||
|
||||
ignore@5.3.2:
|
||||
resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==}
|
||||
engines: {node: '>= 4'}
|
||||
|
|
@ -962,6 +996,10 @@ packages:
|
|||
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
|
||||
engines: {node: '>=0.8.19'}
|
||||
|
||||
index-to-position@1.2.0:
|
||||
resolution: {integrity: sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
inflight@1.0.6:
|
||||
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
|
||||
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
|
||||
|
|
@ -1004,6 +1042,10 @@ packages:
|
|||
resolution: {integrity: sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==}
|
||||
hasBin: true
|
||||
|
||||
js-levenshtein@1.1.6:
|
||||
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
js-tokens@4.0.0:
|
||||
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
|
||||
|
||||
|
|
@ -1022,6 +1064,9 @@ packages:
|
|||
json-schema-traverse@0.4.1:
|
||||
resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==}
|
||||
|
||||
json-schema-traverse@1.0.0:
|
||||
resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1:
|
||||
resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==}
|
||||
|
||||
|
|
@ -1070,6 +1115,10 @@ packages:
|
|||
minimatch@3.1.5:
|
||||
resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==}
|
||||
|
||||
minimatch@5.1.9:
|
||||
resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
minimatch@9.0.9:
|
||||
resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==}
|
||||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
|
|
@ -1106,6 +1155,18 @@ packages:
|
|||
once@1.4.0:
|
||||
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==}
|
||||
|
||||
openapi-fetch@0.13.8:
|
||||
resolution: {integrity: sha512-yJ4QKRyNxE44baQ9mY5+r/kAzZ8yXMemtNAOFwOzRXJscdjSxxzWSNlyBAr+o5JjkUw9Lc3W7OIoca0cY3PYnQ==}
|
||||
|
||||
openapi-typescript-helpers@0.0.15:
|
||||
resolution: {integrity: sha512-opyTPaunsklCBpTK8JGef6mfPhLSnyy5a0IN9vKtx3+4aExf+KxEqYwIy3hqkedXIB97u357uLMJsOnm3GVjsw==}
|
||||
|
||||
openapi-typescript@7.13.0:
|
||||
resolution: {integrity: sha512-EFP392gcqXS7ntPvbhBzbF8TyBA+baIYEm791Hy5YkjDYKTnk/Tn5OQeKm5BIZvJihpp8Zzr4hzx0Irde1LNGQ==}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
typescript: ^5.x
|
||||
|
||||
optionator@0.9.4:
|
||||
resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==}
|
||||
engines: {node: '>= 0.8.0'}
|
||||
|
|
@ -1122,6 +1183,10 @@ packages:
|
|||
resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
parse-json@8.3.0:
|
||||
resolution: {integrity: sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1160,6 +1225,10 @@ packages:
|
|||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
pluralize@8.0.0:
|
||||
resolution: {integrity: sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
postcss-import@15.1.0:
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
|
|
@ -1238,6 +1307,10 @@ packages:
|
|||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
require-from-string@2.0.2:
|
||||
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
resolve-from@4.0.0:
|
||||
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
|
||||
engines: {node: '>=4'}
|
||||
|
|
@ -1315,6 +1388,10 @@ packages:
|
|||
engines: {node: '>=16 || 14 >=14.17'}
|
||||
hasBin: true
|
||||
|
||||
supports-color@10.2.2:
|
||||
resolution: {integrity: sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
supports-color@7.2.0:
|
||||
resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -1372,6 +1449,10 @@ packages:
|
|||
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
type-fest@4.41.0:
|
||||
resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==}
|
||||
engines: {node: '>=16'}
|
||||
|
||||
typescript@5.9.3:
|
||||
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||
engines: {node: '>=14.17'}
|
||||
|
|
@ -1383,6 +1464,9 @@ packages:
|
|||
peerDependencies:
|
||||
browserslist: '>= 4.21.0'
|
||||
|
||||
uri-js-replace@1.0.1:
|
||||
resolution: {integrity: sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==}
|
||||
|
||||
uri-js@4.4.1:
|
||||
resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==}
|
||||
|
||||
|
|
@ -1449,6 +1533,13 @@ packages:
|
|||
yallist@3.1.1:
|
||||
resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==}
|
||||
|
||||
yaml-ast-parser@0.0.43:
|
||||
resolution: {integrity: sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==}
|
||||
|
||||
yargs-parser@21.1.1:
|
||||
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
yocto-queue@0.1.0:
|
||||
resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==}
|
||||
engines: {node: '>=10'}
|
||||
|
|
@ -1478,7 +1569,7 @@ snapshots:
|
|||
'@babel/types': 7.29.0
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
convert-source-map: 2.0.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
gensync: 1.0.0-beta.2
|
||||
json5: 2.2.3
|
||||
semver: 6.3.1
|
||||
|
|
@ -1560,7 +1651,7 @@ snapshots:
|
|||
'@babel/parser': 7.29.0
|
||||
'@babel/template': 7.28.6
|
||||
'@babel/types': 7.29.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
|
|
@ -1657,7 +1748,7 @@ snapshots:
|
|||
'@eslint/eslintrc@2.1.4':
|
||||
dependencies:
|
||||
ajv: 6.14.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
espree: 9.6.1
|
||||
globals: 13.24.0
|
||||
ignore: 5.3.2
|
||||
|
|
@ -1673,7 +1764,7 @@ snapshots:
|
|||
'@humanwhocodes/config-array@0.13.0':
|
||||
dependencies:
|
||||
'@humanwhocodes/object-schema': 2.0.3
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
minimatch: 3.1.5
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
|
@ -1713,6 +1804,29 @@ snapshots:
|
|||
'@nodelib/fs.scandir': 2.1.5
|
||||
fastq: 1.20.1
|
||||
|
||||
'@redocly/ajv@8.11.2':
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
json-schema-traverse: 1.0.0
|
||||
require-from-string: 2.0.2
|
||||
uri-js-replace: 1.0.1
|
||||
|
||||
'@redocly/config@0.22.0': {}
|
||||
|
||||
'@redocly/openapi-core@1.34.10(supports-color@10.2.2)':
|
||||
dependencies:
|
||||
'@redocly/ajv': 8.11.2
|
||||
'@redocly/config': 0.22.0
|
||||
colorette: 1.4.0
|
||||
https-proxy-agent: 7.0.6(supports-color@10.2.2)
|
||||
js-levenshtein: 1.1.6
|
||||
js-yaml: 4.1.1
|
||||
minimatch: 5.1.9
|
||||
pluralize: 8.0.0
|
||||
yaml-ast-parser: 0.0.43
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@rolldown/pluginutils@1.0.0-beta.27': {}
|
||||
|
||||
'@rollup/rollup-android-arm-eabi@4.59.0':
|
||||
|
|
@ -1884,7 +1998,7 @@ snapshots:
|
|||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3)
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
eslint: 8.57.1
|
||||
optionalDependencies:
|
||||
typescript: 5.9.3
|
||||
|
|
@ -1900,7 +2014,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/typescript-estree': 7.18.0(typescript@5.9.3)
|
||||
'@typescript-eslint/utils': 7.18.0(eslint@8.57.1)(typescript@5.9.3)
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
eslint: 8.57.1
|
||||
ts-api-utils: 1.4.3(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
|
|
@ -1914,7 +2028,7 @@ snapshots:
|
|||
dependencies:
|
||||
'@typescript-eslint/types': 7.18.0
|
||||
'@typescript-eslint/visitor-keys': 7.18.0
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
globby: 11.1.0
|
||||
is-glob: 4.0.3
|
||||
minimatch: 9.0.9
|
||||
|
|
@ -1961,6 +2075,8 @@ snapshots:
|
|||
|
||||
acorn@8.16.0: {}
|
||||
|
||||
agent-base@7.1.4: {}
|
||||
|
||||
ajv@6.14.0:
|
||||
dependencies:
|
||||
fast-deep-equal: 3.1.3
|
||||
|
|
@ -1968,6 +2084,8 @@ snapshots:
|
|||
json-schema-traverse: 0.4.1
|
||||
uri-js: 4.4.1
|
||||
|
||||
ansi-colors@4.1.3: {}
|
||||
|
||||
ansi-regex@5.0.1: {}
|
||||
|
||||
ansi-styles@4.3.0:
|
||||
|
|
@ -2034,6 +2152,8 @@ snapshots:
|
|||
ansi-styles: 4.3.0
|
||||
supports-color: 7.2.0
|
||||
|
||||
change-case@5.4.4: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
|
|
@ -2058,6 +2178,8 @@ snapshots:
|
|||
|
||||
color-name@1.1.4: {}
|
||||
|
||||
colorette@1.4.0: {}
|
||||
|
||||
commander@4.1.1: {}
|
||||
|
||||
concat-map@0.0.1: {}
|
||||
|
|
@ -2076,9 +2198,11 @@ snapshots:
|
|||
|
||||
csstype@3.2.3: {}
|
||||
|
||||
debug@4.4.3:
|
||||
debug@4.4.3(supports-color@10.2.2):
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
optionalDependencies:
|
||||
supports-color: 10.2.2
|
||||
|
||||
deep-is@0.1.4: {}
|
||||
|
||||
|
|
@ -2157,7 +2281,7 @@ snapshots:
|
|||
ajv: 6.14.0
|
||||
chalk: 4.1.2
|
||||
cross-spawn: 7.0.6
|
||||
debug: 4.4.3
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
doctrine: 3.0.0
|
||||
escape-string-regexp: 4.0.0
|
||||
eslint-scope: 7.2.2
|
||||
|
|
@ -2297,6 +2421,13 @@ snapshots:
|
|||
dependencies:
|
||||
function-bind: 1.1.2
|
||||
|
||||
https-proxy-agent@7.0.6(supports-color@10.2.2):
|
||||
dependencies:
|
||||
agent-base: 7.1.4
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
ignore@5.3.2: {}
|
||||
|
||||
import-fresh@3.3.1:
|
||||
|
|
@ -2306,6 +2437,8 @@ snapshots:
|
|||
|
||||
imurmurhash@0.1.4: {}
|
||||
|
||||
index-to-position@1.2.0: {}
|
||||
|
||||
inflight@1.0.6:
|
||||
dependencies:
|
||||
once: 1.4.0
|
||||
|
|
@ -2337,6 +2470,8 @@ snapshots:
|
|||
|
||||
jiti@1.21.7: {}
|
||||
|
||||
js-levenshtein@1.1.6: {}
|
||||
|
||||
js-tokens@4.0.0: {}
|
||||
|
||||
js-yaml@4.1.1:
|
||||
|
|
@ -2349,6 +2484,8 @@ snapshots:
|
|||
|
||||
json-schema-traverse@0.4.1: {}
|
||||
|
||||
json-schema-traverse@1.0.0: {}
|
||||
|
||||
json-stable-stringify-without-jsonify@1.0.1: {}
|
||||
|
||||
json5@2.2.3: {}
|
||||
|
|
@ -2391,6 +2528,10 @@ snapshots:
|
|||
dependencies:
|
||||
brace-expansion: 1.1.12
|
||||
|
||||
minimatch@5.1.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
||||
minimatch@9.0.9:
|
||||
dependencies:
|
||||
brace-expansion: 2.0.2
|
||||
|
|
@ -2419,6 +2560,22 @@ snapshots:
|
|||
dependencies:
|
||||
wrappy: 1.0.2
|
||||
|
||||
openapi-fetch@0.13.8:
|
||||
dependencies:
|
||||
openapi-typescript-helpers: 0.0.15
|
||||
|
||||
openapi-typescript-helpers@0.0.15: {}
|
||||
|
||||
openapi-typescript@7.13.0(typescript@5.9.3):
|
||||
dependencies:
|
||||
'@redocly/openapi-core': 1.34.10(supports-color@10.2.2)
|
||||
ansi-colors: 4.1.3
|
||||
change-case: 5.4.4
|
||||
parse-json: 8.3.0
|
||||
supports-color: 10.2.2
|
||||
typescript: 5.9.3
|
||||
yargs-parser: 21.1.1
|
||||
|
||||
optionator@0.9.4:
|
||||
dependencies:
|
||||
deep-is: 0.1.4
|
||||
|
|
@ -2440,6 +2597,12 @@ snapshots:
|
|||
dependencies:
|
||||
callsites: 3.1.0
|
||||
|
||||
parse-json@8.3.0:
|
||||
dependencies:
|
||||
'@babel/code-frame': 7.29.0
|
||||
index-to-position: 1.2.0
|
||||
type-fest: 4.41.0
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-is-absolute@1.0.1: {}
|
||||
|
|
@ -2460,6 +2623,8 @@ snapshots:
|
|||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pluralize@8.0.0: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.5.8):
|
||||
dependencies:
|
||||
postcss: 8.5.8
|
||||
|
|
@ -2520,6 +2685,8 @@ snapshots:
|
|||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
require-from-string@2.0.2: {}
|
||||
|
||||
resolve-from@4.0.0: {}
|
||||
|
||||
resolve@1.22.11:
|
||||
|
|
@ -2607,6 +2774,8 @@ snapshots:
|
|||
tinyglobby: 0.2.15
|
||||
ts-interface-checker: 0.1.13
|
||||
|
||||
supports-color@10.2.2: {}
|
||||
|
||||
supports-color@7.2.0:
|
||||
dependencies:
|
||||
has-flag: 4.0.0
|
||||
|
|
@ -2678,6 +2847,8 @@ snapshots:
|
|||
|
||||
type-fest@0.20.2: {}
|
||||
|
||||
type-fest@4.41.0: {}
|
||||
|
||||
typescript@5.9.3: {}
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
||||
|
|
@ -2686,6 +2857,8 @@ snapshots:
|
|||
escalade: 3.2.0
|
||||
picocolors: 1.1.1
|
||||
|
||||
uri-js-replace@1.0.1: {}
|
||||
|
||||
uri-js@4.4.1:
|
||||
dependencies:
|
||||
punycode: 2.3.1
|
||||
|
|
@ -2718,4 +2891,8 @@ snapshots:
|
|||
|
||||
yallist@3.1.1: {}
|
||||
|
||||
yaml-ast-parser@0.0.43: {}
|
||||
|
||||
yargs-parser@21.1.1: {}
|
||||
|
||||
yocto-queue@0.1.0: {}
|
||||
|
|
|
|||
|
|
@ -1,78 +1,96 @@
|
|||
import type {
|
||||
User,
|
||||
OAuthProvider,
|
||||
ApiToken,
|
||||
CreateTokenRequest,
|
||||
CreateTokenResponse,
|
||||
ApiResponse,
|
||||
} from './types'
|
||||
import createClient from 'openapi-fetch'
|
||||
import type { paths } from './generated/schema'
|
||||
import type { CreateTokenRequest, CreateTokenResponse, User } from './types'
|
||||
|
||||
// 基础 fetch 封装
|
||||
async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
|
||||
const res = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options?.headers,
|
||||
},
|
||||
})
|
||||
const client = createClient<paths>({ baseUrl: '' })
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: 'Request failed' }))
|
||||
throw new Error(error.message || `HTTP ${res.status}`)
|
||||
function getCsrfToken(): string | null {
|
||||
const match = document.cookie.match(/(?:^|; )XSRF-TOKEN=([^;]+)/)
|
||||
return match ? decodeURIComponent(match[1]) : null
|
||||
}
|
||||
|
||||
function withCsrf(headers?: HeadersInit): HeadersInit {
|
||||
const csrfToken = getCsrfToken()
|
||||
if (!csrfToken) {
|
||||
return headers ?? {}
|
||||
}
|
||||
|
||||
return res.json()
|
||||
return {
|
||||
...headers,
|
||||
'X-XSRF-TOKEN': csrfToken,
|
||||
}
|
||||
}
|
||||
|
||||
async function unwrap<T>(promise: Promise<{ data?: T; error?: unknown; response: Response }>): Promise<T> {
|
||||
const { data, error, response } = await promise
|
||||
if (response.status === 401) {
|
||||
throw new Error('HTTP 401')
|
||||
}
|
||||
if (error) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
if (data === undefined) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
return data
|
||||
}
|
||||
|
||||
export async function getCurrentUser(): Promise<User | null> {
|
||||
try {
|
||||
return await unwrap(client.GET('/api/v1/auth/me'))
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'HTTP 401') {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
// Auth API
|
||||
export const authApi = {
|
||||
// 获取当前用户信息
|
||||
async getMe(): Promise<User | null> {
|
||||
try {
|
||||
return await fetchJson<User>('/api/v1/auth/me')
|
||||
} catch (error) {
|
||||
// 401 表示未登录,返回 null
|
||||
if (error instanceof Error && error.message.includes('401')) {
|
||||
return null
|
||||
}
|
||||
throw error
|
||||
getMe: getCurrentUser,
|
||||
|
||||
async getProviders() {
|
||||
const response = await unwrap(client.GET('/api/v1/auth/providers'))
|
||||
return response.data
|
||||
},
|
||||
|
||||
async logout(): Promise<void> {
|
||||
const { response, error } = await client.POST('/api/v1/auth/logout', {
|
||||
headers: withCsrf(),
|
||||
})
|
||||
if (error || (response.status !== 200 && response.status !== 204)) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
},
|
||||
|
||||
// 获取可用的 OAuth 提供商
|
||||
async getProviders(): Promise<OAuthProvider[]> {
|
||||
const response = await fetchJson<ApiResponse<OAuthProvider[]>>('/api/v1/auth/providers')
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 登出
|
||||
async logout(): Promise<void> {
|
||||
await fetch('/api/v1/auth/logout', { method: 'POST' })
|
||||
},
|
||||
}
|
||||
|
||||
// Token API
|
||||
export const tokenApi = {
|
||||
// 获取所有 Token
|
||||
async getTokens(): Promise<ApiToken[]> {
|
||||
const response = await fetchJson<ApiResponse<ApiToken[]>>('/api/v1/tokens')
|
||||
async getTokens() {
|
||||
const response = await unwrap(client.GET('/api/v1/tokens'))
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 创建新 Token
|
||||
async createToken(request: CreateTokenRequest): Promise<CreateTokenResponse> {
|
||||
const response = await fetchJson<ApiResponse<CreateTokenResponse>>('/api/v1/tokens', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(request),
|
||||
})
|
||||
const response = await unwrap(client.POST('/api/v1/tokens', {
|
||||
headers: withCsrf({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: request,
|
||||
}))
|
||||
return response.data
|
||||
},
|
||||
|
||||
// 删除 Token
|
||||
async deleteToken(tokenId: number): Promise<void> {
|
||||
await fetch(`/api/v1/tokens/${tokenId}`, {
|
||||
method: 'DELETE',
|
||||
const { response, error } = await client.DELETE('/api/v1/tokens/{id}', {
|
||||
params: {
|
||||
path: {
|
||||
id: tokenId,
|
||||
},
|
||||
},
|
||||
headers: withCsrf(),
|
||||
})
|
||||
if (error || response.status !== 204) {
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
125
web/src/api/generated/schema.d.ts
vendored
Normal file
125
web/src/api/generated/schema.d.ts
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
export interface paths {
|
||||
'/api/v1/auth/me': {
|
||||
get: {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['User']
|
||||
}
|
||||
}
|
||||
401: {
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'/api/v1/auth/providers': {
|
||||
get: {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ApiResponse_OAuthProviderList']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'/api/v1/auth/logout': {
|
||||
post: {
|
||||
responses: {
|
||||
200: {
|
||||
content?: never
|
||||
}
|
||||
204: {
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'/api/v1/tokens': {
|
||||
get: {
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ApiResponse_ApiTokenList']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
post: {
|
||||
requestBody: {
|
||||
content: {
|
||||
'application/json': components['schemas']['CreateTokenRequest']
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
200: {
|
||||
content: {
|
||||
'application/json': components['schemas']['ApiResponse_CreateTokenResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
'/api/v1/tokens/{id}': {
|
||||
delete: {
|
||||
parameters: {
|
||||
path: {
|
||||
id: number
|
||||
}
|
||||
}
|
||||
responses: {
|
||||
204: {
|
||||
content?: never
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface components {
|
||||
schemas: {
|
||||
User: {
|
||||
userId: number
|
||||
displayName: string
|
||||
email: string
|
||||
avatarUrl: string
|
||||
oauthProvider: string
|
||||
platformRoles: string[]
|
||||
}
|
||||
OAuthProvider: {
|
||||
id: string
|
||||
name: string
|
||||
authorizationUrl: string
|
||||
}
|
||||
ApiToken: {
|
||||
id: number
|
||||
name: string
|
||||
tokenPrefix: string
|
||||
createdAt: string
|
||||
expiresAt: string
|
||||
lastUsedAt: string
|
||||
}
|
||||
CreateTokenRequest: {
|
||||
name: string
|
||||
scopes?: string[]
|
||||
}
|
||||
CreateTokenResponse: {
|
||||
token: string
|
||||
id: number
|
||||
name: string
|
||||
tokenPrefix: string
|
||||
createdAt: string
|
||||
expiresAt: string
|
||||
}
|
||||
ApiResponse_OAuthProviderList: {
|
||||
data: components['schemas']['OAuthProvider'][]
|
||||
}
|
||||
ApiResponse_ApiTokenList: {
|
||||
data: components['schemas']['ApiToken'][]
|
||||
}
|
||||
ApiResponse_CreateTokenResponse: {
|
||||
data: components['schemas']['CreateTokenResponse']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,52 +1,7 @@
|
|||
// API 类型定义
|
||||
import type { components } from './generated/schema'
|
||||
|
||||
export interface User {
|
||||
userId: number
|
||||
displayName: string
|
||||
email: string
|
||||
avatarUrl: string
|
||||
oauthProvider: string
|
||||
platformRoles: string[]
|
||||
}
|
||||
|
||||
export interface OAuthProvider {
|
||||
id: string
|
||||
name: string
|
||||
authorizationUrl: string
|
||||
}
|
||||
|
||||
export interface ApiToken {
|
||||
tokenId: number
|
||||
name: string
|
||||
tokenPrefix: string
|
||||
createdAt: string
|
||||
lastUsedAt: string | null
|
||||
expiresAt: string | null
|
||||
}
|
||||
|
||||
export interface CreateTokenRequest {
|
||||
name: string
|
||||
expiresInDays?: number
|
||||
}
|
||||
|
||||
export interface CreateTokenResponse {
|
||||
token: string
|
||||
tokenId: number
|
||||
name: string
|
||||
tokenPrefix: string
|
||||
createdAt: string
|
||||
expiresAt: string | null
|
||||
}
|
||||
|
||||
export interface ApiResponse<T> {
|
||||
data: T
|
||||
message?: string
|
||||
}
|
||||
|
||||
export interface ApiError {
|
||||
error: string
|
||||
message: string
|
||||
path: string
|
||||
timestamp: string
|
||||
requestId: string
|
||||
}
|
||||
export type User = components['schemas']['User']
|
||||
export type OAuthProvider = components['schemas']['OAuthProvider']
|
||||
export type ApiToken = components['schemas']['ApiToken']
|
||||
export type CreateTokenRequest = components['schemas']['CreateTokenRequest']
|
||||
export type CreateTokenResponse = components['schemas']['CreateTokenResponse']
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import { createRouter, createRoute, createRootRoute } from '@tanstack/react-router'
|
||||
import { createRouter, createRoute, createRootRoute, redirect } from '@tanstack/react-router'
|
||||
import { Layout } from './layout'
|
||||
import { HomePage } from '@/pages/home'
|
||||
import { LoginPage } from '@/pages/login'
|
||||
import { DashboardPage } from '@/pages/dashboard'
|
||||
import { getCurrentUser } from '@/api/client'
|
||||
|
||||
const rootRoute = createRootRoute({
|
||||
component: Layout,
|
||||
|
|
@ -23,6 +24,13 @@ const loginRoute = createRoute({
|
|||
const dashboardRoute = createRoute({
|
||||
getParentRoute: () => rootRoute,
|
||||
path: '/dashboard',
|
||||
beforeLoad: async () => {
|
||||
const user = await getCurrentUser()
|
||||
if (!user) {
|
||||
throw redirect({ to: '/login' })
|
||||
}
|
||||
return { user }
|
||||
},
|
||||
component: DashboardPage,
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
import { useEffect } from 'react'
|
||||
import { useNavigate } from '@tanstack/react-router'
|
||||
import { useAuth } from './use-auth'
|
||||
|
||||
interface AuthGuardProps {
|
||||
children: React.ReactNode
|
||||
}
|
||||
|
||||
export function AuthGuard({ children }: AuthGuardProps) {
|
||||
const { isAuthenticated, isLoading } = useAuth()
|
||||
const navigate = useNavigate()
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoading && !isAuthenticated) {
|
||||
navigate({ to: '/login' })
|
||||
}
|
||||
}, [isLoading, isAuthenticated, navigate])
|
||||
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center">
|
||||
<div className="text-muted-foreground">加载中...</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (!isAuthenticated) {
|
||||
return null
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
}
|
||||
|
|
@ -71,7 +71,7 @@ export function TokenList() {
|
|||
</TableHeader>
|
||||
<TableBody>
|
||||
{tokens.map((token) => (
|
||||
<TableRow key={token.tokenId}>
|
||||
<TableRow key={token.id}>
|
||||
<TableCell className="font-medium">{token.name}</TableCell>
|
||||
<TableCell>
|
||||
<code className="text-sm bg-muted px-2 py-1 rounded">
|
||||
|
|
@ -85,7 +85,7 @@ export function TokenList() {
|
|||
<Button
|
||||
variant="destructive"
|
||||
size="sm"
|
||||
onClick={() => handleDelete(token.tokenId, token.name)}
|
||||
onClick={() => handleDelete(token.id, token.name)}
|
||||
disabled={deleteMutation.isPending}
|
||||
>
|
||||
删除
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
import { AuthGuard } from '@/features/auth/auth-guard'
|
||||
import { useAuth } from '@/features/auth/use-auth'
|
||||
import { TokenList } from '@/features/token/token-list'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/shared/ui/card'
|
||||
|
|
@ -7,57 +6,55 @@ export function DashboardPage() {
|
|||
const { user } = useAuth()
|
||||
|
||||
return (
|
||||
<AuthGuard>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
管理你的账户和 API Tokens
|
||||
</p>
|
||||
</div>
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h1 className="text-3xl font-bold">Dashboard</h1>
|
||||
<p className="text-muted-foreground mt-1">
|
||||
管理你的账户和 API Tokens
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用户信息</CardTitle>
|
||||
<CardDescription>你的账户详情</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{user?.avatarUrl && (
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.displayName}
|
||||
className="h-16 w-16 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<div className="text-lg font-semibold">{user?.displayName}</div>
|
||||
<div className="text-sm text-muted-foreground">{user?.email}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
通过 {user?.oauthProvider} 登录
|
||||
</div>
|
||||
<Card>
|
||||
<CardHeader>
|
||||
<CardTitle>用户信息</CardTitle>
|
||||
<CardDescription>你的账户详情</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="flex items-center gap-4">
|
||||
{user?.avatarUrl && (
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.displayName}
|
||||
className="h-16 w-16 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<div className="space-y-1">
|
||||
<div className="text-lg font-semibold">{user?.displayName}</div>
|
||||
<div className="text-sm text-muted-foreground">{user?.email}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
通过 {user?.oauthProvider} 登录
|
||||
</div>
|
||||
</div>
|
||||
{user?.platformRoles && user.platformRoles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">平台角色</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{user.platformRoles.map((role) => (
|
||||
<span
|
||||
key={role}
|
||||
className="inline-flex items-center rounded-md bg-primary/10 px-2 py-1 text-xs font-medium text-primary"
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
{user?.platformRoles && user.platformRoles.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<div className="text-sm font-medium">平台角色</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{user.platformRoles.map((role) => (
|
||||
<span
|
||||
key={role}
|
||||
className="inline-flex items-center rounded-md bg-primary/10 px-2 py-1 text-xs font-medium text-primary"
|
||||
>
|
||||
{role}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
<TokenList />
|
||||
</div>
|
||||
</AuthGuard>
|
||||
<TokenList />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue