From 0b1607630fcd2f9dc9bb418dc032a699e86d4cd2 Mon Sep 17 00:00:00 2001 From: vsxd Date: Thu, 12 Mar 2026 00:37:51 +0800 Subject: [PATCH] fix(phase1): close auth and frontend acceptance gaps --- Makefile | 17 +- docker-compose.prod.yml | 86 ++++++++ server/Dockerfile | 14 ++ .../skillhub/controller/CliController.java | 33 +++ .../skillhub/controller/TokenController.java | 12 +- .../controller/CliControllerTest.java | 59 ++++++ .../token/ApiTokenAuthenticationFilter.java | 9 +- web/Dockerfile | 14 ++ web/nginx.conf | 48 +++++ web/package.json | 8 +- web/pnpm-lock.yaml | 195 +++++++++++++++++- web/src/api/client.ts | 128 +++++++----- web/src/api/generated/schema.d.ts | 125 +++++++++++ web/src/api/types.ts | 57 +---- web/src/app/router.tsx | 10 +- web/src/features/auth/auth-guard.tsx | 32 --- web/src/features/token/token-list.tsx | 4 +- web/src/pages/dashboard.tsx | 93 ++++----- 18 files changed, 737 insertions(+), 207 deletions(-) create mode 100644 docker-compose.prod.yml create mode 100644 server/Dockerfile create mode 100644 server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/CliController.java create mode 100644 server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/CliControllerTest.java create mode 100644 web/Dockerfile create mode 100644 web/nginx.conf create mode 100644 web/src/api/generated/schema.d.ts delete mode 100644 web/src/features/auth/auth-guard.tsx diff --git a/Makefile b/Makefile index 10eebc19..e9814200 100644 --- a/Makefile +++ b/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 diff --git a/docker-compose.prod.yml b/docker-compose.prod.yml new file mode 100644 index 00000000..3d5b7374 --- /dev/null +++ b/docker-compose.prod.yml @@ -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: diff --git a/server/Dockerfile b/server/Dockerfile new file mode 100644 index 00000000..d55fc51b --- /dev/null +++ b/server/Dockerfile @@ -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"] diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/CliController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/CliController.java new file mode 100644 index 00000000..af13b7b9 --- /dev/null +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/CliController.java @@ -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> 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() + ) + )); + } +} diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/TokenController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/TokenController.java index 1c06c248..7cd8fcb4 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/TokenController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/TokenController.java @@ -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() : "" + ) )); } diff --git a/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/CliControllerTest.java b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/CliControllerTest.java new file mode 100644 index 00000000..0a35ce1b --- /dev/null +++ b/server/skillhub-app/src/test/java/com/iflytek/skillhub/controller/CliControllerTest.java @@ -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")); + } +} diff --git a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java index c16ff398..4302f0e2 100644 --- a/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java +++ b/server/skillhub-auth/src/main/java/com/iflytek/skillhub/auth/token/ApiTokenAuthenticationFilter.java @@ -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/")); + } } diff --git a/web/Dockerfile b/web/Dockerfile new file mode 100644 index 00000000..b5ea803f --- /dev/null +++ b/web/Dockerfile @@ -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 diff --git a/web/nginx.conf b/web/nginx.conf new file mode 100644 index 00000000..e08010f5 --- /dev/null +++ b/web/nginx.conf @@ -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; + } +} diff --git a/web/package.json b/web/package.json index 35b3e242..baee58da 100644 --- a/web/package.json +++ b/web/package.json @@ -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" } } diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 2e44c790..7966e1f1 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -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: {} diff --git a/web/src/api/client.ts b/web/src/api/client.ts index f7fceb4e..e954c96a 100644 --- a/web/src/api/client.ts +++ b/web/src/api/client.ts @@ -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(url: string, options?: RequestInit): Promise { - const res = await fetch(url, { - ...options, - headers: { - 'Content-Type': 'application/json', - ...options?.headers, - }, - }) +const client = createClient({ 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(promise: Promise<{ data?: T; error?: unknown; response: Response }>): Promise { + 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 { + 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 { - try { - return await fetchJson('/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 { + 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 { - const response = await fetchJson>('/api/v1/auth/providers') - return response.data - }, - - // 登出 - async logout(): Promise { - await fetch('/api/v1/auth/logout', { method: 'POST' }) - }, } -// Token API export const tokenApi = { - // 获取所有 Token - async getTokens(): Promise { - const response = await fetchJson>('/api/v1/tokens') + async getTokens() { + const response = await unwrap(client.GET('/api/v1/tokens')) return response.data }, - // 创建新 Token async createToken(request: CreateTokenRequest): Promise { - const response = await fetchJson>('/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 { - 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}`) + } }, } diff --git a/web/src/api/generated/schema.d.ts b/web/src/api/generated/schema.d.ts new file mode 100644 index 00000000..028515fc --- /dev/null +++ b/web/src/api/generated/schema.d.ts @@ -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'] + } + } +} diff --git a/web/src/api/types.ts b/web/src/api/types.ts index 36c09a98..501a2586 100644 --- a/web/src/api/types.ts +++ b/web/src/api/types.ts @@ -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 { - 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'] diff --git a/web/src/app/router.tsx b/web/src/app/router.tsx index 124c6c99..e3bb544b 100644 --- a/web/src/app/router.tsx +++ b/web/src/app/router.tsx @@ -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, }) diff --git a/web/src/features/auth/auth-guard.tsx b/web/src/features/auth/auth-guard.tsx deleted file mode 100644 index 84a43aa6..00000000 --- a/web/src/features/auth/auth-guard.tsx +++ /dev/null @@ -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 ( -
-
加载中...
-
- ) - } - - if (!isAuthenticated) { - return null - } - - return <>{children} -} diff --git a/web/src/features/token/token-list.tsx b/web/src/features/token/token-list.tsx index 5ab761c3..a73492bb 100644 --- a/web/src/features/token/token-list.tsx +++ b/web/src/features/token/token-list.tsx @@ -71,7 +71,7 @@ export function TokenList() { {tokens.map((token) => ( - + {token.name} @@ -85,7 +85,7 @@ export function TokenList() {