Compare commits

..

1 Commits

Author SHA1 Message Date
Tim
bf169932b3 refactor: use iconpark in dropdown 2025-09-06 10:07:19 +08:00
163 changed files with 396 additions and 3932 deletions

View File

@@ -1,47 +0,0 @@
name: Deploy Documentation
on:
workflow_call:
inputs:
build-id:
required: false
type: string
workflow_dispatch:
permissions:
contents: write
jobs:
build-docs:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v3
with:
fetch-depth: 1
- name: Log build
run: echo "Running documentation deployment from build ${{ inputs.build-id }}"
- name: Setup Bun
uses: oven-sh/setup-bun@v1
with:
bun-version: latest
- name: Install Bun dependencies
run: bun install
working-directory: ./docs
- name: Generate API MDX
run: bun run generate
working-directory: ./docs
- name: Build documentation
run: bun run build
working-directory: ./docs
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4
with:
branch: gh-pages
folder: ./docs/out

View File

@@ -5,9 +5,6 @@ on:
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
permissions:
contents: write
jobs: jobs:
build-and-deploy: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -24,11 +21,3 @@ jobs:
key: ${{ secrets.SSH_KEY }} key: ${{ secrets.SSH_KEY }}
script: bash /opt/openisle/deploy-staging.sh script: bash /opt/openisle/deploy-staging.sh
deploy-docs:
needs: build-and-deploy
if: ${{ success() }}
uses: ./.github/workflows/deploy-docs.yml
secrets: inherit
with:
build-id: ${{ github.run_id }}

2
.gitignore vendored
View File

@@ -21,8 +21,6 @@ dist
npm-debug.log* npm-debug.log*
yarn-debug.log* yarn-debug.log*
yarn-error.log* yarn-error.log*
pnpm-lock.yaml
pnpm-workspace.yaml
# env # env
*.env *.env

View File

@@ -70,9 +70,7 @@ SERVER_PORT=8082
#### 配置 IDEA 参数 #### 配置 IDEA 参数
- 设置 JDK 版本为 java 17 - 设置 JDK 版本为 java 17
- 设置 VM Option最好运行在其他端口非 `8080`,这里设置 `8081` - 设置 VM Option最好运行在其他端口非 `8080`,这里设置 `8081`
若上面在环境变量中设置了端口,那这里就不需要再额外设置
```shell ```shell
-Dserver.port=8081 -Dserver.port=8081
@@ -103,27 +101,14 @@ SERVER_PORT=8082
``` ```
3. 执行 [`db/init/init_script.sql`](backend/src/main/resources/db/init/init_script.sql) 脚本,导入基本的数据 3. 执行 [`db/init/init_script.sql`](backend/src/main/resources/db/init/init_script.sql) 脚本,导入基本的数据
管理员:**admin/123456**
普通用户1**user1/123456**
普通用户2**user2/123456**
![初始化脚本](assets/contributing/resources_img.png) ![初始化脚本](assets/contributing/resources_img.png)
#### 配置 Redis 4. 处理完环境问题直接跑起来就能通了
填写环境变量 `.env` 中的 Redis 相关配置并启动 Redis ![运行画面](assets/contributing/backend_img_4.png)
```ini
REDIS_HOST=<Redis 地址>
REDIS_PORT=<Redis 端口>
```
处理完环境问题直接跑起来就能通了
![运行画面](assets/contributing/backend_img_4.png)
### Docker 环境 ### Docker 环境
#### 配置环境变量 #### 配置环境变量
```shell ```shell
@@ -192,8 +177,6 @@ cd frontend_nuxt/
cp .env.dev.example .env cp .env.dev.example .env
``` ```
若依赖本机部署的后端,需要修改 `.env` 中的 `NUXT_PUBLIC_API_BASE_URL` 值与后端服务端口一致
### 安装依赖和运行 ### 安装依赖和运行
前端安装依赖并启动服务。 前端安装依赖并启动服务。
@@ -210,7 +193,7 @@ npm run dev
## 其他配置 ## 其他配置
### 配置第三方登录,这里以 GitHub 为例: 配置第三方登录,这里以 GitHub 为例:
- 修改 `application.properties` 配置 - 修改 `application.properties` 配置
@@ -225,30 +208,3 @@ npm run dev
![github配置](assets/contributing/github_img.png) ![github配置](assets/contributing/github_img.png)
![github配置2](assets/contributing/github_img_2.png) ![github配置2](assets/contributing/github_img_2.png)
### 配置 Resend 邮箱服务
https://resend.com/emails 创建账号并登录
- `Domains` -> `Add Domain`
![image-20250906150459400](assets/contributing/image-20250906150459400.png)
- 填写域名
![image-20250906150541817](assets/contributing/image-20250906150541817.png)
- 等待一段时间后解析成功,创建 key
`API Keys` -> `Create API Key`,输入名称,设置 `Permission` 为 `Sending access`
**Key 只能查看一次,务必保存下来**
![image-20250906150811572](assets/contributing/image-20250906150811572.png)
![image-20250906150924975](assets/contributing/image-20250906150924975.png)
![image-20250906150944130](assets/contributing/image-20250906150944130.png)
- 修改 `.env` 配置中的 `RESEND_API_KEY` 和 `RESEND_FROM_EMAIL`
`RESEND_FROM_EMAIL` **noreply@域名**
`RESEND_API_KEY`**刚刚复制的 Key**
![image-20250906151218330](assets/contributing/image-20250906151218330.png)
## 开源共建和API文档
- API文档: https://openisle-docs.netlify.app/docs/openapi

View File

@@ -4,8 +4,6 @@
高效的开源社区前后端平台 高效的开源社区前后端平台
<br><br><br> <br><br><br>
<img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200"> <img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
<br><br><br>
<a href="https://hellogithub.com/repository/nagisa77/OpenIsle" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8605546658d94cbab45182af2a02e4c8&claim_uid=p5GNFTtZl6HBAYQ" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p> </p>
## 💡 简介 ## 💡 简介

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 113 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 12 KiB

View File

@@ -13,13 +13,8 @@ JWT_RESET_SECRET=<jwt reset secret>
JWT_INVITE_SECRET=<jwt invite secret> JWT_INVITE_SECRET=<jwt invite secret>
JWT_EXPIRATION=2592000000 JWT_EXPIRATION=2592000000
# === Redis ===
REDIS_HOST=<Redis 地址>
REDIS_PORT=<Redis 端口>
# === Resend === # === Resend ===
RESEND_API_KEY=<你的resend-api-key> RESEND_API_KEY=<你的resend-api-key>
RESEND_FROM_EMAIL=<你的 resend 发送邮箱>
# === COS === # === COS ===
# COS_BASE_URL=https://<你的cos>.cos.ap-guangzhou.myqcloud.com # COS_BASE_URL=https://<你的cos>.cos.ap-guangzhou.myqcloud.com

View File

@@ -22,8 +22,6 @@ import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration; import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
/** /**
* Redis 缓存配置类 * Redis 缓存配置类
@@ -38,12 +36,6 @@ public class CachingConfig {
public static final String TAG_CACHE_NAME="openisle_tags"; public static final String TAG_CACHE_NAME="openisle_tags";
// 分类缓存名 // 分类缓存名
public static final String CATEGORY_CACHE_NAME="openisle_categories"; public static final String CATEGORY_CACHE_NAME="openisle_categories";
// 在线人数缓存名
public static final String ONLINE_CACHE_NAME="openisle_online";
// 注册验证码
public static final String VERIFY_CACHE_NAME="openisle_verify";
// 发帖频率限制
public static final String LIMIT_CACHE_NAME="openisle_limit";
/** /**
* 自定义Redis的序列化器 * 自定义Redis的序列化器
@@ -86,16 +78,13 @@ public class CachingConfig {
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.disableCachingNullValues(); // 禁止缓存 null 值 .disableCachingNullValues(); // 禁止缓存 null 值
// 个别缓存单独设置TTL时间
// 个别缓存单独设置 TTL 时间 // Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>(); // cacheConfigs.put("openisle_tags", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ZERO));
RedisCacheConfiguration oneHourConfig = config.entryTtl(Duration.ofHours(1)); // cacheConfigs.put("openisle_categories", RedisCacheConfiguration.defaultCacheConfig().entryTtl(Duration.ZERO));
cacheConfigs.put(TAG_CACHE_NAME, oneHourConfig);
cacheConfigs.put(CATEGORY_CACHE_NAME, oneHourConfig);
return RedisCacheManager.builder(connectionFactory) return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(config) .cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigs)
.build(); .build();
} }

View File

@@ -5,21 +5,13 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import java.util.List;
import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@Configuration @Configuration
@RequiredArgsConstructor
public class OpenApiConfig { public class OpenApiConfig {
private final SpringDocProperties springDocProperties;
@Value("${springdoc.info.title}") @Value("${springdoc.info.title}")
private String title; private String title;
@@ -38,23 +30,19 @@ public class OpenApiConfig {
@Bean @Bean
public OpenAPI openAPI() { public OpenAPI openAPI() {
SecurityScheme securityScheme = new SecurityScheme() SecurityScheme securityScheme = new SecurityScheme()
.type(SecurityScheme.Type.HTTP) .type(SecurityScheme.Type.HTTP)
.scheme(scheme.toLowerCase()) .scheme(scheme.toLowerCase())
.bearerFormat("JWT") .bearerFormat("JWT")
.in(SecurityScheme.In.HEADER) .in(SecurityScheme.In.HEADER)
.name(header); .name(header);
List<Server> servers = springDocProperties.getServers().stream()
.map(s -> new Server().url(s.getUrl()).description(s.getDescription()))
.collect(Collectors.toList());
return new OpenAPI() return new OpenAPI()
.servers(servers)
.info(new Info() .info(new Info()
.title(title) .title(title)
.description(description) .description(description)
.version(version)) .version(version))
.components(new Components().addSecuritySchemes("JWT", securityScheme)) .components(new Components()
.addSecuritySchemes("JWT", securityScheme))
.addSecurityItem(new SecurityRequirement().addList("JWT")); .addSecurityItem(new SecurityRequirement().addList("JWT"));
} }
} }

View File

@@ -90,9 +90,6 @@ public class SecurityConfig {
"http://192.168.7.98", "http://192.168.7.98",
"http://192.168.7.98:3000", "http://192.168.7.98:3000",
"https://petstore.swagger.io", "https://petstore.swagger.io",
// 允许自建OpenAPI地址
"https://docs.open-isle.com",
"https://www.docs.open-isle.com",
websiteUrl, websiteUrl,
websiteUrl.replace("://www.", "://") websiteUrl.replace("://www.", "://")
)); ));
@@ -132,8 +129,6 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll() .requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
.requestMatchers(HttpMethod.GET, "/api/channels").permitAll() .requestMatchers(HttpMethod.GET, "/api/channels").permitAll()
.requestMatchers(HttpMethod.GET, "/api/rss").permitAll() .requestMatchers(HttpMethod.GET, "/api/rss").permitAll()
.requestMatchers(HttpMethod.GET, "/api/online/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/online/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll() .requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll()
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll() .requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN") .requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
@@ -188,8 +183,7 @@ public class SecurityConfig {
} }
} else if (!uri.startsWith("/api/auth") && !publicGet } else if (!uri.startsWith("/api/auth") && !publicGet
&& !uri.startsWith("/api/ws") && !uri.startsWith("/api/sockjs") && !uri.startsWith("/api/ws") && !uri.startsWith("/api/sockjs")
&& !uri.startsWith("/api/v3/api-docs") && !uri.startsWith("/api/v3/api-docs")) {
&& !uri.startsWith("/api/online")) {
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.setContentType("application/json"); response.setContentType("application/json");
response.getWriter().write("{\"error\": \"Missing token\"}"); response.getWriter().write("{\"error\": \"Missing token\"}");

View File

@@ -1,20 +0,0 @@
package com.openisle.config;
import java.util.ArrayList;
import java.util.List;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "springdoc.api-docs")
public class SpringDocProperties {
private List<ServerConfig> servers = new ArrayList<>();
@Data
public static class ServerConfig {
private String url;
private String description;
}
}

View File

@@ -1,36 +0,0 @@
package com.openisle.config;
import com.openisle.model.Role;
import com.openisle.model.User;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Component;
/**
* Ensure a dedicated "system" user exists for internal operations.
*/
@Component
@RequiredArgsConstructor
public class SystemUserInitializer implements CommandLineRunner {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
@Override
public void run(String... args) {
userRepository.findByUsername("system").orElseGet(() -> {
User system = new User();
system.setUsername("system");
system.setEmail("system@openisle.local");
// todo(tim): raw password 采用环境变量
system.setPassword(passwordEncoder.encode("system"));
system.setRole(Role.USER);
system.setVerified(true);
system.setApproved(true);
system.setAvatar("https://openisle-1307107697.cos.ap-guangzhou.myqcloud.com/assert/image.png");
return userRepository.save(system);
});
}
}

View File

@@ -12,12 +12,6 @@ import com.openisle.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -31,9 +25,6 @@ public class ActivityController {
private final ActivityMapper activityMapper; private final ActivityMapper activityMapper;
@GetMapping @GetMapping
@Operation(summary = "List activities", description = "Retrieve all activities")
@ApiResponse(responseCode = "200", description = "List of activities",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ActivityDto.class))))
public List<ActivityDto> list() { public List<ActivityDto> list() {
return activityService.list().stream() return activityService.list().stream()
.map(activityMapper::toDto) .map(activityMapper::toDto)
@@ -41,9 +32,6 @@ public class ActivityController {
} }
@GetMapping("/milk-tea") @GetMapping("/milk-tea")
@Operation(summary = "Milk tea info", description = "Get milk tea activity information")
@ApiResponse(responseCode = "200", description = "Milk tea info",
content = @Content(schema = @Schema(implementation = MilkTeaInfoDto.class)))
public MilkTeaInfoDto milkTea() { public MilkTeaInfoDto milkTea() {
Activity a = activityService.getByType(ActivityType.MILK_TEA); Activity a = activityService.getByType(ActivityType.MILK_TEA);
long count = activityService.countParticipants(a); long count = activityService.countParticipants(a);
@@ -57,10 +45,6 @@ public class ActivityController {
} }
@PostMapping("/milk-tea/redeem") @PostMapping("/milk-tea/redeem")
@Operation(summary = "Redeem milk tea", description = "Redeem milk tea activity reward")
@ApiResponse(responseCode = "200", description = "Redeem result",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
@SecurityRequirement(name = "JWT")
public java.util.Map<String, String> redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) { public java.util.Map<String, String> redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow(); User user = userService.findByIdentifier(auth.getName()).orElseThrow();
Activity a = activityService.getByType(ActivityType.MILK_TEA); Activity a = activityService.getByType(ActivityType.MILK_TEA);

View File

@@ -3,11 +3,6 @@ package com.openisle.controller;
import com.openisle.dto.CommentDto; import com.openisle.dto.CommentDto;
import com.openisle.mapper.CommentMapper; import com.openisle.mapper.CommentMapper;
import com.openisle.service.CommentService; import com.openisle.service.CommentService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -23,19 +18,11 @@ public class AdminCommentController {
private final CommentMapper commentMapper; private final CommentMapper commentMapper;
@PostMapping("/{id}/pin") @PostMapping("/{id}/pin")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Pin comment", description = "Pin a comment by its id")
@ApiResponse(responseCode = "200", description = "Pinned comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
public CommentDto pin(@PathVariable Long id, Authentication auth) { public CommentDto pin(@PathVariable Long id, Authentication auth) {
return commentMapper.toDto(commentService.pinComment(auth.getName(), id)); return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
} }
@PostMapping("/{id}/unpin") @PostMapping("/{id}/unpin")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Unpin comment", description = "Remove pin from a comment")
@ApiResponse(responseCode = "200", description = "Unpinned comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
public CommentDto unpin(@PathVariable Long id, Authentication auth) { public CommentDto unpin(@PathVariable Long id, Authentication auth) {
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id)); return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));
} }

View File

@@ -5,11 +5,6 @@ import com.openisle.service.AiUsageService;
import com.openisle.service.PasswordValidator; import com.openisle.service.PasswordValidator;
import com.openisle.service.PostService; import com.openisle.service.PostService;
import com.openisle.service.RegisterModeService; import com.openisle.service.RegisterModeService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -23,10 +18,6 @@ public class AdminConfigController {
private final RegisterModeService registerModeService; private final RegisterModeService registerModeService;
@GetMapping @GetMapping
@SecurityRequirement(name = "JWT")
@Operation(summary = "Get configuration", description = "Retrieve application configuration settings")
@ApiResponse(responseCode = "200", description = "Current configuration",
content = @Content(schema = @Schema(implementation = ConfigDto.class)))
public ConfigDto getConfig() { public ConfigDto getConfig() {
ConfigDto dto = new ConfigDto(); ConfigDto dto = new ConfigDto();
dto.setPublishMode(postService.getPublishMode()); dto.setPublishMode(postService.getPublishMode());
@@ -37,10 +28,6 @@ public class AdminConfigController {
} }
@PostMapping @PostMapping
@SecurityRequirement(name = "JWT")
@Operation(summary = "Update configuration", description = "Update application configuration settings")
@ApiResponse(responseCode = "200", description = "Updated configuration",
content = @Content(schema = @Schema(implementation = ConfigDto.class)))
public ConfigDto updateConfig(@RequestBody ConfigDto dto) { public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
if (dto.getPublishMode() != null) { if (dto.getPublishMode() != null) {
postService.setPublishMode(dto.getPublishMode()); postService.setPublishMode(dto.getPublishMode());

View File

@@ -1,10 +1,5 @@
package com.openisle.controller; package com.openisle.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Map; import java.util.Map;
@@ -15,10 +10,6 @@ import java.util.Map;
@RestController @RestController
public class AdminController { public class AdminController {
@GetMapping("/api/admin/hello") @GetMapping("/api/admin/hello")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Admin greeting", description = "Returns a greeting for admin users")
@ApiResponse(responseCode = "200", description = "Greeting payload",
content = @Content(schema = @Schema(implementation = Map.class)))
public Map<String, String> adminHello() { public Map<String, String> adminHello() {
return Map.of("message", "Hello, Admin User"); return Map.of("message", "Hello, Admin User");
} }

View File

@@ -3,12 +3,6 @@ package com.openisle.controller;
import com.openisle.dto.PostSummaryDto; import com.openisle.dto.PostSummaryDto;
import com.openisle.mapper.PostMapper; import com.openisle.mapper.PostMapper;
import com.openisle.service.PostService; import com.openisle.service.PostService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -26,10 +20,6 @@ public class AdminPostController {
private final PostMapper postMapper; private final PostMapper postMapper;
@GetMapping("/pending") @GetMapping("/pending")
@SecurityRequirement(name = "JWT")
@Operation(summary = "List pending posts", description = "Retrieve posts awaiting approval")
@ApiResponse(responseCode = "200", description = "Pending posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> pendingPosts() { public List<PostSummaryDto> pendingPosts() {
return postService.listPendingPosts().stream() return postService.listPendingPosts().stream()
.map(postMapper::toSummaryDto) .map(postMapper::toSummaryDto)
@@ -37,56 +27,32 @@ public class AdminPostController {
} }
@PostMapping("/{id}/approve") @PostMapping("/{id}/approve")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Approve post", description = "Approve a pending post")
@ApiResponse(responseCode = "200", description = "Approved post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto approve(@PathVariable Long id) { public PostSummaryDto approve(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.approvePost(id)); return postMapper.toSummaryDto(postService.approvePost(id));
} }
@PostMapping("/{id}/reject") @PostMapping("/{id}/reject")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Reject post", description = "Reject a pending post")
@ApiResponse(responseCode = "200", description = "Rejected post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto reject(@PathVariable Long id) { public PostSummaryDto reject(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.rejectPost(id)); return postMapper.toSummaryDto(postService.rejectPost(id));
} }
@PostMapping("/{id}/pin") @PostMapping("/{id}/pin")
@SecurityRequirement(name = "JWT") public PostSummaryDto pin(@PathVariable Long id) {
@Operation(summary = "Pin post", description = "Pin a post to the top") return postMapper.toSummaryDto(postService.pinPost(id));
@ApiResponse(responseCode = "200", description = "Pinned post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto pin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.pinPost(id, auth.getName()));
} }
@PostMapping("/{id}/unpin") @PostMapping("/{id}/unpin")
@SecurityRequirement(name = "JWT") public PostSummaryDto unpin(@PathVariable Long id) {
@Operation(summary = "Unpin post", description = "Remove a post from the top") return postMapper.toSummaryDto(postService.unpinPost(id));
@ApiResponse(responseCode = "200", description = "Unpinned post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto unpin(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.unpinPost(id, auth.getName()));
} }
@PostMapping("/{id}/rss-exclude") @PostMapping("/{id}/rss-exclude")
@SecurityRequirement(name = "JWT") public PostSummaryDto excludeFromRss(@PathVariable Long id) {
@Operation(summary = "Exclude from RSS", description = "Exclude a post from RSS feed") return postMapper.toSummaryDto(postService.excludeFromRss(id));
@ApiResponse(responseCode = "200", description = "Updated post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto excludeFromRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.excludeFromRss(id, auth.getName()));
} }
@PostMapping("/{id}/rss-include") @PostMapping("/{id}/rss-include")
@SecurityRequirement(name = "JWT") public PostSummaryDto includeInRss(@PathVariable Long id) {
@Operation(summary = "Include in RSS", description = "Include a post in the RSS feed") return postMapper.toSummaryDto(postService.includeInRss(id));
@ApiResponse(responseCode = "200", description = "Updated post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto includeInRss(@PathVariable Long id, org.springframework.security.core.Authentication auth) {
return postMapper.toSummaryDto(postService.includeInRss(id, auth.getName()));
} }
} }

View File

@@ -5,12 +5,6 @@ import com.openisle.mapper.TagMapper;
import com.openisle.model.Tag; import com.openisle.model.Tag;
import com.openisle.service.PostService; import com.openisle.service.PostService;
import com.openisle.service.TagService; import com.openisle.service.TagService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -26,10 +20,6 @@ public class AdminTagController {
private final TagMapper tagMapper; private final TagMapper tagMapper;
@GetMapping("/pending") @GetMapping("/pending")
@SecurityRequirement(name = "JWT")
@Operation(summary = "List pending tags", description = "Retrieve tags awaiting approval")
@ApiResponse(responseCode = "200", description = "Pending tags",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
public List<TagDto> pendingTags() { public List<TagDto> pendingTags() {
return tagService.listPendingTags().stream() return tagService.listPendingTags().stream()
.map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId()))) .map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId())))
@@ -37,10 +27,6 @@ public class AdminTagController {
} }
@PostMapping("/{id}/approve") @PostMapping("/{id}/approve")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Approve tag", description = "Approve a pending tag")
@ApiResponse(responseCode = "200", description = "Approved tag",
content = @Content(schema = @Schema(implementation = TagDto.class)))
public TagDto approve(@PathVariable Long id) { public TagDto approve(@PathVariable Long id) {
Tag tag = tagService.approveTag(id); Tag tag = tagService.approveTag(id);
long count = postService.countPostsByTag(tag.getId()); long count = postService.countPostsByTag(tag.getId());

View File

@@ -6,9 +6,6 @@ import com.openisle.model.User;
import com.openisle.service.EmailSender; import com.openisle.service.EmailSender;
import com.openisle.repository.NotificationRepository; import com.openisle.repository.NotificationRepository;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -25,9 +22,6 @@ public class AdminUserController {
private String websiteUrl; private String websiteUrl;
@PostMapping("/{id}/approve") @PostMapping("/{id}/approve")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Approve user", description = "Approve a pending user registration")
@ApiResponse(responseCode = "200", description = "User approved")
public ResponseEntity<?> approve(@PathVariable Long id) { public ResponseEntity<?> approve(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow(); User user = userRepository.findById(id).orElseThrow();
user.setApproved(true); user.setApproved(true);
@@ -39,9 +33,6 @@ public class AdminUserController {
} }
@PostMapping("/{id}/reject") @PostMapping("/{id}/reject")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Reject user", description = "Reject a pending user registration")
@ApiResponse(responseCode = "200", description = "User rejected")
public ResponseEntity<?> reject(@PathVariable Long id) { public ResponseEntity<?> reject(@PathVariable Long id) {
User user = userRepository.findById(id).orElseThrow(); User user = userRepository.findById(id).orElseThrow();
user.setApproved(false); user.setApproved(false);

View File

@@ -9,11 +9,6 @@ import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.Map; import java.util.Map;
@@ -26,10 +21,6 @@ public class AiController {
private final AiUsageService aiUsageService; private final AiUsageService aiUsageService;
@PostMapping("/format") @PostMapping("/format")
@Operation(summary = "Format markdown", description = "Format text via AI")
@ApiResponse(responseCode = "200", description = "Formatted content",
content = @Content(schema = @Schema(implementation = Map.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<Map<String, String>> format(@RequestBody Map<String, String> req, public ResponseEntity<Map<String, String>> format(@RequestBody Map<String, String> req,
Authentication auth) { Authentication auth) {
String text = req.get("text"); String text = req.get("text");

View File

@@ -1,27 +1,18 @@
package com.openisle.controller; package com.openisle.controller;
import com.openisle.config.CachingConfig;
import com.openisle.dto.*; import com.openisle.dto.*;
import com.openisle.exception.FieldException; import com.openisle.exception.FieldException;
import com.openisle.model.RegisterMode; import com.openisle.model.RegisterMode;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.service.*; import com.openisle.service.*;
import com.openisle.util.VerifyType;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/auth")
@@ -52,9 +43,6 @@ public class AuthController {
private boolean loginCaptchaEnabled; private boolean loginCaptchaEnabled;
@PostMapping("/register") @PostMapping("/register")
@Operation(summary = "Register user", description = "Register a new user account")
@ApiResponse(responseCode = "200", description = "Registration result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> register(@RequestBody RegisterRequest req) { public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha")); return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
@@ -68,8 +56,7 @@ public class AuthController {
User user = userService.registerWithInvite( User user = userService.registerWithInvite(
req.getUsername(), req.getEmail(), req.getPassword()); req.getUsername(), req.getEmail(), req.getPassword());
inviteService.consume(req.getInviteToken(), user.getUsername()); inviteService.consume(req.getInviteToken(), user.getUsername());
// 发送确认邮件 emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
userService.sendVerifyMail(user, VerifyType.REGISTER);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(user.getUsername()), "token", jwtService.generateToken(user.getUsername()),
"reason_code", "INVITE_APPROVED" "reason_code", "INVITE_APPROVED"
@@ -83,8 +70,7 @@ public class AuthController {
} }
User user = userService.register( User user = userService.register(
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode()); req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
// 发送确认邮件 emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
userService.sendVerifyMail(user, VerifyType.REGISTER);
if (!user.isApproved()) { if (!user.isApproved()) {
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason()); notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
} }
@@ -92,16 +78,14 @@ public class AuthController {
} }
@PostMapping("/verify") @PostMapping("/verify")
@Operation(summary = "Verify account", description = "Verify registration code")
@ApiResponse(responseCode = "200", description = "Verification result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) { public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
Optional<User> userOpt = userService.findByUsername(req.getUsername()); boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
}
boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.REGISTER);
if (ok) { if (ok) {
Optional<User> userOpt = userService.findByUsername(req.getUsername());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
}
User user = userOpt.get(); User user = userOpt.get();
if (user.isApproved()) { if (user.isApproved()) {
@@ -122,9 +106,6 @@ public class AuthController {
} }
@PostMapping("/login") @PostMapping("/login")
@Operation(summary = "Login", description = "Authenticate with username/email and password")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> login(@RequestBody LoginRequest req) { public ResponseEntity<?> login(@RequestBody LoginRequest req) {
if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha")); return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
@@ -141,7 +122,7 @@ public class AuthController {
User user = userOpt.get(); User user = userOpt.get();
if (!user.isVerified()) { if (!user.isVerified()) {
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode()); user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
userService.sendVerifyMail(user, VerifyType.REGISTER); emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
return ResponseEntity.badRequest().body(Map.of( return ResponseEntity.badRequest().body(Map.of(
"error", "User not verified", "error", "User not verified",
"reason_code", "NOT_VERIFIED", "reason_code", "NOT_VERIFIED",
@@ -163,9 +144,6 @@ public class AuthController {
} }
@PostMapping("/google") @PostMapping("/google")
@Operation(summary = "Login with Google", description = "Authenticate using Google account")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) { public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -213,9 +191,6 @@ public class AuthController {
@PostMapping("/reason") @PostMapping("/reason")
@Operation(summary = "Submit register reason", description = "Submit registration reason for approval")
@ApiResponse(responseCode = "200", description = "Submission result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> reason(@RequestBody MakeReasonRequest req) { public ResponseEntity<?> reason(@RequestBody MakeReasonRequest req) {
String username = jwtService.validateAndGetSubjectForReason(req.getToken()); String username = jwtService.validateAndGetSubjectForReason(req.getToken());
Optional<User> userOpt = userService.findByUsername(username); Optional<User> userOpt = userService.findByUsername(username);
@@ -244,9 +219,6 @@ public class AuthController {
} }
@PostMapping("/github") @PostMapping("/github")
@Operation(summary = "Login with GitHub", description = "Authenticate using GitHub account")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) { public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -295,9 +267,6 @@ public class AuthController {
} }
@PostMapping("/discord") @PostMapping("/discord")
@Operation(summary = "Login with Discord", description = "Authenticate using Discord account")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) { public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -345,9 +314,6 @@ public class AuthController {
} }
@PostMapping("/twitter") @PostMapping("/twitter")
@Operation(summary = "Login with Twitter", description = "Authenticate using Twitter account")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) { public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -396,9 +362,6 @@ public class AuthController {
} }
@PostMapping("/telegram") @PostMapping("/telegram")
@Operation(summary = "Login with Telegram", description = "Authenticate using Telegram data")
@ApiResponse(responseCode = "200", description = "Authentication result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> loginWithTelegram(@RequestBody TelegramLoginRequest req) { public ResponseEntity<?> loginWithTelegram(@RequestBody TelegramLoginRequest req) {
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
@@ -444,37 +407,24 @@ public class AuthController {
} }
@GetMapping("/check") @GetMapping("/check")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Check token", description = "Validate JWT token")
@ApiResponse(responseCode = "200", description = "Token valid",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> checkToken() { public ResponseEntity<?> checkToken() {
return ResponseEntity.ok(Map.of("valid", true)); return ResponseEntity.ok(Map.of("valid", true));
} }
@PostMapping("/forgot/send") @PostMapping("/forgot/send")
@Operation(summary = "Send reset code", description = "Send verification code for password reset")
@ApiResponse(responseCode = "200", description = "Sending result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) { public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) {
Optional<User> userOpt = userService.findByEmail(req.getEmail()); Optional<User> userOpt = userService.findByEmail(req.getEmail());
if (userOpt.isEmpty()) { if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "User not found")); return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
} }
userService.sendVerifyMail(userOpt.get(), VerifyType.RESET_PASSWORD); String code = userService.generatePasswordResetCode(req.getEmail());
emailService.sendEmail(req.getEmail(), "请填写验证码以重置密码", "您的验证码是" + code);
return ResponseEntity.ok(Map.of("message", "Verification code sent")); return ResponseEntity.ok(Map.of("message", "Verification code sent"));
} }
@PostMapping("/forgot/verify") @PostMapping("/forgot/verify")
@Operation(summary = "Verify reset code", description = "Verify password reset code")
@ApiResponse(responseCode = "200", description = "Verification result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) { public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
Optional<User> userOpt = userService.findByEmail(req.getEmail()); boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
}
boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.RESET_PASSWORD);
if (ok) { if (ok) {
String username = userService.findByEmail(req.getEmail()).get().getUsername(); String username = userService.findByEmail(req.getEmail()).get().getUsername();
return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username))); return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username)));
@@ -483,9 +433,6 @@ public class AuthController {
} }
@PostMapping("/forgot/reset") @PostMapping("/forgot/reset")
@Operation(summary = "Reset password", description = "Reset user password after verification")
@ApiResponse(responseCode = "200", description = "Reset result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) { public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) {
String username = jwtService.validateAndGetSubjectForReset(req.getToken()); String username = jwtService.validateAndGetSubjectForReset(req.getToken());
try { try {

View File

@@ -10,11 +10,6 @@ import com.openisle.service.CategoryService;
import com.openisle.service.PostService; import com.openisle.service.PostService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -30,9 +25,6 @@ public class CategoryController {
private final CategoryMapper categoryMapper; private final CategoryMapper categoryMapper;
@PostMapping @PostMapping
@Operation(summary = "Create category", description = "Create a new category")
@ApiResponse(responseCode = "200", description = "Created category",
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
public CategoryDto create(@RequestBody CategoryRequest req) { public CategoryDto create(@RequestBody CategoryRequest req) {
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByCategory(c.getId()); long count = postService.countPostsByCategory(c.getId());
@@ -40,9 +32,6 @@ public class CategoryController {
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "Update category", description = "Update an existing category")
@ApiResponse(responseCode = "200", description = "Updated category",
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) { public CategoryDto update(@PathVariable Long id, @RequestBody CategoryRequest req) {
Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByCategory(c.getId()); long count = postService.countPostsByCategory(c.getId());
@@ -50,16 +39,11 @@ public class CategoryController {
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "Delete category", description = "Remove a category by id")
@ApiResponse(responseCode = "200", description = "Category deleted")
public void delete(@PathVariable Long id) { public void delete(@PathVariable Long id) {
categoryService.deleteCategory(id); categoryService.deleteCategory(id);
} }
@GetMapping @GetMapping
@Operation(summary = "List categories", description = "Get all categories")
@ApiResponse(responseCode = "200", description = "List of categories",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CategoryDto.class))))
public List<CategoryDto> list() { public List<CategoryDto> list() {
List<Category> all = categoryService.listCategories(); List<Category> all = categoryService.listCategories();
List<Long> ids = all.stream().map(Category::getId).toList(); List<Long> ids = all.stream().map(Category::getId).toList();
@@ -71,9 +55,6 @@ public class CategoryController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "Get category", description = "Get category by id")
@ApiResponse(responseCode = "200", description = "Category detail",
content = @Content(schema = @Schema(implementation = CategoryDto.class)))
public CategoryDto get(@PathVariable Long id) { public CategoryDto get(@PathVariable Long id) {
Category c = categoryService.getCategory(id); Category c = categoryService.getCategory(id);
long count = postService.countPostsByCategory(c.getId()); long count = postService.countPostsByCategory(c.getId());
@@ -81,9 +62,6 @@ public class CategoryController {
} }
@GetMapping("/{id}/posts") @GetMapping("/{id}/posts")
@Operation(summary = "List posts by category", description = "Get posts under a category")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> listPostsByCategory(@PathVariable Long id, public List<PostSummaryDto> listPostsByCategory(@PathVariable Long id,
@RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize) { @RequestParam(value = "pageSize", required = false) Integer pageSize) {

View File

@@ -8,12 +8,6 @@ import com.openisle.service.MessageService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
@@ -32,28 +26,16 @@ public class ChannelController {
} }
@GetMapping @GetMapping
@Operation(summary = "List channels", description = "List channels for the current user")
@ApiResponse(responseCode = "200", description = "Channels",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ChannelDto.class))))
@SecurityRequirement(name = "JWT")
public List<ChannelDto> listChannels(Authentication auth) { public List<ChannelDto> listChannels(Authentication auth) {
return channelService.listChannels(getCurrentUserId(auth)); return channelService.listChannels(getCurrentUserId(auth));
} }
@PostMapping("/{channelId}/join") @PostMapping("/{channelId}/join")
@Operation(summary = "Join channel", description = "Join a channel")
@ApiResponse(responseCode = "200", description = "Joined channel",
content = @Content(schema = @Schema(implementation = ChannelDto.class)))
@SecurityRequirement(name = "JWT")
public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) { public ChannelDto joinChannel(@PathVariable Long channelId, Authentication auth) {
return channelService.joinChannel(channelId, getCurrentUserId(auth)); return channelService.joinChannel(channelId, getCurrentUserId(auth));
} }
@GetMapping("/unread-count") @GetMapping("/unread-count")
@Operation(summary = "Unread count", description = "Get unread channel count")
@ApiResponse(responseCode = "200", description = "Unread count",
content = @Content(schema = @Schema(implementation = Long.class)))
@SecurityRequirement(name = "JWT")
public long unreadCount(Authentication auth) { public long unreadCount(Authentication auth) {
return messageService.getUnreadChannelCount(getCurrentUserId(auth)); return messageService.getUnreadChannelCount(getCurrentUserId(auth));
} }

View File

@@ -14,12 +14,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -42,10 +36,6 @@ public class CommentController {
private boolean commentCaptchaEnabled; private boolean commentCaptchaEnabled;
@PostMapping("/posts/{postId}/comments") @PostMapping("/posts/{postId}/comments")
@Operation(summary = "Create comment", description = "Add a comment to a post")
@ApiResponse(responseCode = "200", description = "Created comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<CommentDto> createComment(@PathVariable Long postId, public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
@RequestBody CommentRequest req, @RequestBody CommentRequest req,
Authentication auth) { Authentication auth) {
@@ -63,10 +53,6 @@ public class CommentController {
} }
@PostMapping("/comments/{commentId}/replies") @PostMapping("/comments/{commentId}/replies")
@Operation(summary = "Reply to comment", description = "Reply to an existing comment")
@ApiResponse(responseCode = "200", description = "Reply created",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId, public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
@RequestBody CommentRequest req, @RequestBody CommentRequest req,
Authentication auth) { Authentication auth) {
@@ -83,9 +69,6 @@ public class CommentController {
} }
@GetMapping("/posts/{postId}/comments") @GetMapping("/posts/{postId}/comments")
@Operation(summary = "List comments", description = "List comments for a post")
@ApiResponse(responseCode = "200", description = "Comments",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentDto.class))))
public List<CommentDto> listComments(@PathVariable Long postId, public List<CommentDto> listComments(@PathVariable Long postId,
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) { @RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) {
log.debug("listComments called for post {} with sort {}", postId, sort); log.debug("listComments called for post {} with sort {}", postId, sort);
@@ -97,9 +80,6 @@ public class CommentController {
} }
@DeleteMapping("/comments/{id}") @DeleteMapping("/comments/{id}")
@Operation(summary = "Delete comment", description = "Delete a comment")
@ApiResponse(responseCode = "200", description = "Deleted")
@SecurityRequirement(name = "JWT")
public void deleteComment(@PathVariable Long id, Authentication auth) { public void deleteComment(@PathVariable Long id, Authentication auth) {
log.debug("deleteComment called by user {} for comment {}", auth.getName(), id); log.debug("deleteComment called by user {} for comment {}", auth.getName(), id);
commentService.deleteComment(auth.getName(), id); commentService.deleteComment(auth.getName(), id);
@@ -107,20 +87,12 @@ public class CommentController {
} }
@PostMapping("/comments/{id}/pin") @PostMapping("/comments/{id}/pin")
@Operation(summary = "Pin comment", description = "Pin a comment")
@ApiResponse(responseCode = "200", description = "Pinned comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
@SecurityRequirement(name = "JWT")
public CommentDto pinComment(@PathVariable Long id, Authentication auth) { public CommentDto pinComment(@PathVariable Long id, Authentication auth) {
log.debug("pinComment called by user {} for comment {}", auth.getName(), id); log.debug("pinComment called by user {} for comment {}", auth.getName(), id);
return commentMapper.toDto(commentService.pinComment(auth.getName(), id)); return commentMapper.toDto(commentService.pinComment(auth.getName(), id));
} }
@PostMapping("/comments/{id}/unpin") @PostMapping("/comments/{id}/unpin")
@Operation(summary = "Unpin comment", description = "Unpin a comment")
@ApiResponse(responseCode = "200", description = "Unpinned comment",
content = @Content(schema = @Schema(implementation = CommentDto.class)))
@SecurityRequirement(name = "JWT")
public CommentDto unpinComment(@PathVariable Long id, Authentication auth) { public CommentDto unpinComment(@PathVariable Long id, Authentication auth) {
log.debug("unpinComment called by user {} for comment {}", auth.getName(), id); log.debug("unpinComment called by user {} for comment {}", auth.getName(), id);
return commentMapper.toDto(commentService.unpinComment(auth.getName(), id)); return commentMapper.toDto(commentService.unpinComment(auth.getName(), id));

View File

@@ -6,10 +6,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@@ -37,9 +33,6 @@ public class ConfigController {
private final RegisterModeService registerModeService; private final RegisterModeService registerModeService;
@GetMapping("/config") @GetMapping("/config")
@Operation(summary = "Site config", description = "Get site configuration")
@ApiResponse(responseCode = "200", description = "Site configuration",
content = @Content(schema = @Schema(implementation = SiteConfigDto.class)))
public SiteConfigDto getConfig() { public SiteConfigDto getConfig() {
SiteConfigDto resp = new SiteConfigDto(); SiteConfigDto resp = new SiteConfigDto();
resp.setCaptchaEnabled(captchaEnabled); resp.setCaptchaEnabled(captchaEnabled);

View File

@@ -9,11 +9,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@RestController @RestController
@RequestMapping("/api/drafts") @RequestMapping("/api/drafts")
@@ -23,20 +18,12 @@ public class DraftController {
private final DraftMapper draftMapper; private final DraftMapper draftMapper;
@PostMapping @PostMapping
@Operation(summary = "Save draft", description = "Save a draft for current user")
@ApiResponse(responseCode = "200", description = "Draft saved",
content = @Content(schema = @Schema(implementation = DraftDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) { public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds()); Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
return ResponseEntity.ok(draftMapper.toDto(draft)); return ResponseEntity.ok(draftMapper.toDto(draft));
} }
@GetMapping("/me") @GetMapping("/me")
@Operation(summary = "Get my draft", description = "Get current user's draft")
@ApiResponse(responseCode = "200", description = "Draft details",
content = @Content(schema = @Schema(implementation = DraftDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) { public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
return draftService.getDraft(auth.getName()) return draftService.getDraft(auth.getName())
.map(d -> ResponseEntity.ok(draftMapper.toDto(d))) .map(d -> ResponseEntity.ok(draftMapper.toDto(d)))
@@ -44,9 +31,6 @@ public class DraftController {
} }
@DeleteMapping("/me") @DeleteMapping("/me")
@Operation(summary = "Delete my draft", description = "Delete current user's draft")
@ApiResponse(responseCode = "200", description = "Draft deleted")
@SecurityRequirement(name = "JWT")
public ResponseEntity<?> deleteMyDraft(Authentication auth) { public ResponseEntity<?> deleteMyDraft(Authentication auth) {
draftService.deleteDraft(auth.getName()); draftService.deleteDraft(auth.getName());
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();

View File

@@ -1,10 +1,5 @@
package com.openisle.controller; package com.openisle.controller;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Map; import java.util.Map;
@@ -12,10 +7,6 @@ import java.util.Map;
@RestController @RestController
public class HelloController { public class HelloController {
@GetMapping("/api/hello") @GetMapping("/api/hello")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Hello endpoint", description = "Returns a greeting for authenticated users")
@ApiResponse(responseCode = "200", description = "Greeting payload",
content = @Content(schema = @Schema(implementation = Map.class)))
public Map<String, String> hello() { public Map<String, String> hello() {
return Map.of("message", "Hello, Authenticated User"); return Map.of("message", "Hello, Authenticated User");
} }

View File

@@ -6,11 +6,6 @@ import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.Map; import java.util.Map;
@@ -21,10 +16,6 @@ public class InviteController {
private final InviteService inviteService; private final InviteService inviteService;
@PostMapping("/generate") @PostMapping("/generate")
@Operation(summary = "Generate invite", description = "Generate an invite token")
@ApiResponse(responseCode = "200", description = "Invite token",
content = @Content(schema = @Schema(implementation = Map.class)))
@SecurityRequirement(name = "JWT")
public Map<String, String> generate(Authentication auth) { public Map<String, String> generate(Authentication auth) {
String token = inviteService.generate(auth.getName()); String token = inviteService.generate(auth.getName());
return Map.of("token", token); return Map.of("token", token);

View File

@@ -7,12 +7,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
@@ -23,17 +17,11 @@ public class MedalController {
private final MedalService medalService; private final MedalService medalService;
@GetMapping @GetMapping
@Operation(summary = "List medals", description = "List medals for user or globally")
@ApiResponse(responseCode = "200", description = "List of medals",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = MedalDto.class))))
public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) { public List<MedalDto> getMedals(@RequestParam(value = "userId", required = false) Long userId) {
return medalService.getMedals(userId); return medalService.getMedals(userId);
} }
@PostMapping("/select") @PostMapping("/select")
@Operation(summary = "Select medal", description = "Select a medal for current user")
@ApiResponse(responseCode = "200", description = "Medal selected")
@SecurityRequirement(name = "JWT")
public ResponseEntity<Void> selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) { public ResponseEntity<Void> selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) {
try { try {
medalService.selectMedal(auth.getName(), req.getType()); medalService.selectMedal(auth.getName(), req.getType());

View File

@@ -18,12 +18,6 @@ import org.springframework.data.domain.Sort;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
@@ -43,20 +37,12 @@ public class MessageController {
} }
@GetMapping("/conversations") @GetMapping("/conversations")
@Operation(summary = "List conversations", description = "Get all conversations of current user")
@ApiResponse(responseCode = "200", description = "List of conversations",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = ConversationDto.class))))
@SecurityRequirement(name = "JWT")
public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) { public ResponseEntity<List<ConversationDto>> getConversations(Authentication auth) {
List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth)); List<ConversationDto> conversations = messageService.getConversations(getCurrentUserId(auth));
return ResponseEntity.ok(conversations); return ResponseEntity.ok(conversations);
} }
@GetMapping("/conversations/{conversationId}") @GetMapping("/conversations/{conversationId}")
@Operation(summary = "Get conversation", description = "Get messages of a conversation")
@ApiResponse(responseCode = "200", description = "Conversation detail",
content = @Content(schema = @Schema(implementation = ConversationDetailDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<ConversationDetailDto> getMessages(@PathVariable Long conversationId, public ResponseEntity<ConversationDetailDto> getMessages(@PathVariable Long conversationId,
@RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size, @RequestParam(defaultValue = "20") int size,
@@ -67,20 +53,12 @@ public class MessageController {
} }
@PostMapping @PostMapping
@Operation(summary = "Send message", description = "Send a direct message to a user")
@ApiResponse(responseCode = "200", description = "Message sent",
content = @Content(schema = @Schema(implementation = MessageDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<MessageDto> sendMessage(@RequestBody MessageRequest req, Authentication auth) { public ResponseEntity<MessageDto> sendMessage(@RequestBody MessageRequest req, Authentication auth) {
Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent(), req.getReplyToId()); Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent(), req.getReplyToId());
return ResponseEntity.ok(messageService.toDto(message)); return ResponseEntity.ok(messageService.toDto(message));
} }
@PostMapping("/conversations/{conversationId}/messages") @PostMapping("/conversations/{conversationId}/messages")
@Operation(summary = "Send message to conversation", description = "Reply within a conversation")
@ApiResponse(responseCode = "200", description = "Message sent",
content = @Content(schema = @Schema(implementation = MessageDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<MessageDto> sendMessageToConversation(@PathVariable Long conversationId, public ResponseEntity<MessageDto> sendMessageToConversation(@PathVariable Long conversationId,
@RequestBody ChannelMessageRequest req, @RequestBody ChannelMessageRequest req,
Authentication auth) { Authentication auth) {
@@ -89,29 +67,18 @@ public class MessageController {
} }
@PostMapping("/conversations/{conversationId}/read") @PostMapping("/conversations/{conversationId}/read")
@Operation(summary = "Mark conversation read", description = "Mark messages in conversation as read")
@ApiResponse(responseCode = "200", description = "Marked as read")
@SecurityRequirement(name = "JWT")
public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) { public ResponseEntity<Void> markAsRead(@PathVariable Long conversationId, Authentication auth) {
messageService.markConversationAsRead(conversationId, getCurrentUserId(auth)); messageService.markConversationAsRead(conversationId, getCurrentUserId(auth));
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@PostMapping("/conversations") @PostMapping("/conversations")
@Operation(summary = "Find or create conversation", description = "Find existing or create new conversation with recipient")
@ApiResponse(responseCode = "200", description = "Conversation id",
content = @Content(schema = @Schema(implementation = CreateConversationResponse.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<CreateConversationResponse> findOrCreateConversation(@RequestBody CreateConversationRequest req, Authentication auth) { public ResponseEntity<CreateConversationResponse> findOrCreateConversation(@RequestBody CreateConversationRequest req, Authentication auth) {
MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId()); MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId());
return ResponseEntity.ok(new CreateConversationResponse(conversation.getId())); return ResponseEntity.ok(new CreateConversationResponse(conversation.getId()));
} }
@GetMapping("/unread-count") @GetMapping("/unread-count")
@Operation(summary = "Unread message count", description = "Get unread message count for current user")
@ApiResponse(responseCode = "200", description = "Unread count",
content = @Content(schema = @Schema(implementation = Long.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<Long> getUnreadCount(Authentication auth) { public ResponseEntity<Long> getUnreadCount(Authentication auth) {
return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth))); return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth)));
} }

View File

@@ -10,12 +10,6 @@ import com.openisle.service.NotificationService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -29,10 +23,6 @@ public class NotificationController {
private final NotificationMapper notificationMapper; private final NotificationMapper notificationMapper;
@GetMapping @GetMapping
@Operation(summary = "List notifications", description = "Retrieve notifications for the current user")
@ApiResponse(responseCode = "200", description = "Notifications",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))))
@SecurityRequirement(name = "JWT")
public List<NotificationDto> list(@RequestParam(value = "page", defaultValue = "0") int page, public List<NotificationDto> list(@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "30") int size, @RequestParam(value = "size", defaultValue = "30") int size,
Authentication auth) { Authentication auth) {
@@ -42,10 +32,6 @@ public class NotificationController {
} }
@GetMapping("/unread") @GetMapping("/unread")
@Operation(summary = "List unread notifications", description = "Retrieve unread notifications for the current user")
@ApiResponse(responseCode = "200", description = "Unread notifications",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationDto.class))))
@SecurityRequirement(name = "JWT")
public List<NotificationDto> listUnread(@RequestParam(value = "page", defaultValue = "0") int page, public List<NotificationDto> listUnread(@RequestParam(value = "page", defaultValue = "0") int page,
@RequestParam(value = "size", defaultValue = "30") int size, @RequestParam(value = "size", defaultValue = "30") int size,
Authentication auth) { Authentication auth) {
@@ -55,10 +41,6 @@ public class NotificationController {
} }
@GetMapping("/unread-count") @GetMapping("/unread-count")
@Operation(summary = "Unread count", description = "Get count of unread notifications")
@ApiResponse(responseCode = "200", description = "Unread count",
content = @Content(schema = @Schema(implementation = NotificationUnreadCountDto.class)))
@SecurityRequirement(name = "JWT")
public NotificationUnreadCountDto unreadCount(Authentication auth) { public NotificationUnreadCountDto unreadCount(Authentication auth) {
long count = notificationService.countUnread(auth.getName()); long count = notificationService.countUnread(auth.getName());
NotificationUnreadCountDto uc = new NotificationUnreadCountDto(); NotificationUnreadCountDto uc = new NotificationUnreadCountDto();
@@ -67,43 +49,26 @@ public class NotificationController {
} }
@PostMapping("/read") @PostMapping("/read")
@Operation(summary = "Mark notifications read", description = "Mark notifications as read")
@ApiResponse(responseCode = "200", description = "Marked read")
@SecurityRequirement(name = "JWT")
public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) { public void markRead(@RequestBody NotificationMarkReadRequest req, Authentication auth) {
notificationService.markRead(auth.getName(), req.getIds()); notificationService.markRead(auth.getName(), req.getIds());
} }
@GetMapping("/prefs") @GetMapping("/prefs")
@Operation(summary = "List preferences", description = "List notification preferences")
@ApiResponse(responseCode = "200", description = "Preferences",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))))
@SecurityRequirement(name = "JWT")
public List<NotificationPreferenceDto> prefs(Authentication auth) { public List<NotificationPreferenceDto> prefs(Authentication auth) {
return notificationService.listPreferences(auth.getName()); return notificationService.listPreferences(auth.getName());
} }
@PostMapping("/prefs") @PostMapping("/prefs")
@Operation(summary = "Update preference", description = "Update notification preference")
@ApiResponse(responseCode = "200", description = "Preference updated")
@SecurityRequirement(name = "JWT")
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) { public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled()); notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
} }
@GetMapping("/email-prefs") @GetMapping("/email-prefs")
@Operation(summary = "List email preferences", description = "List email notification preferences")
@ApiResponse(responseCode = "200", description = "Email preferences",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = NotificationPreferenceDto.class))))
@SecurityRequirement(name = "JWT")
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) { public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
return notificationService.listEmailPreferences(auth.getName()); return notificationService.listEmailPreferences(auth.getName());
} }
@PostMapping("/email-prefs") @PostMapping("/email-prefs")
@Operation(summary = "Update email preference", description = "Update email notification preference")
@ApiResponse(responseCode = "200", description = "Email preference updated")
@SecurityRequirement(name = "JWT")
public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) { public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled()); notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
} }

View File

@@ -1,42 +0,0 @@
package com.openisle.controller;
import com.openisle.config.CachingConfig;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.time.Duration;
/**
* @author smallclover
* @since 2025-09-05
* 统计在线人数
*/
@RestController
@RequestMapping("/api/online")
@RequiredArgsConstructor
public class OnlineController {
private final RedisTemplate redisTemplate;
private static final String ONLINE_KEY = CachingConfig.ONLINE_CACHE_NAME +":";
@PostMapping("/heartbeat")
@Operation(summary = "Heartbeat", description = "Record user heartbeat")
@ApiResponse(responseCode = "200", description = "Heartbeat recorded")
public void ping(@RequestParam String userId){
redisTemplate.opsForValue().set(ONLINE_KEY+userId,"1", Duration.ofSeconds(150));
}
@GetMapping("/count")
@Operation(summary = "Online count", description = "Get current online user count")
@ApiResponse(responseCode = "200", description = "Online count",
content = @Content(schema = @Schema(implementation = Long.class)))
public long count(){
return redisTemplate.keys(ONLINE_KEY+"*").size();
}
}

View File

@@ -9,12 +9,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -28,10 +22,6 @@ public class PointHistoryController {
private final PointHistoryMapper pointHistoryMapper; private final PointHistoryMapper pointHistoryMapper;
@GetMapping @GetMapping
@Operation(summary = "Point history", description = "List point history for current user")
@ApiResponse(responseCode = "200", description = "List of point histories",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointHistoryDto.class))))
@SecurityRequirement(name = "JWT")
public List<PointHistoryDto> list(Authentication auth) { public List<PointHistoryDto> list(Authentication auth) {
return pointService.listHistory(auth.getName()).stream() return pointService.listHistory(auth.getName()).stream()
.map(pointHistoryMapper::toDto) .map(pointHistoryMapper::toDto)
@@ -39,10 +29,6 @@ public class PointHistoryController {
} }
@GetMapping("/trend") @GetMapping("/trend")
@Operation(summary = "Point trend", description = "Get point trend data for current user")
@ApiResponse(responseCode = "200", description = "Trend data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
@SecurityRequirement(name = "JWT")
public List<Map<String, Object>> trend(Authentication auth, public List<Map<String, Object>> trend(Authentication auth,
@RequestParam(value = "days", defaultValue = "30") int days) { @RequestParam(value = "days", defaultValue = "30") int days) {
return pointService.trend(auth.getName(), days); return pointService.trend(auth.getName(), days);

View File

@@ -9,12 +9,6 @@ import com.openisle.service.UserService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -30,9 +24,6 @@ public class PointMallController {
private final PointGoodMapper pointGoodMapper; private final PointGoodMapper pointGoodMapper;
@GetMapping @GetMapping
@Operation(summary = "List goods", description = "List all point goods")
@ApiResponse(responseCode = "200", description = "List of goods",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PointGoodDto.class))))
public List<PointGoodDto> list() { public List<PointGoodDto> list() {
return pointMallService.listGoods().stream() return pointMallService.listGoods().stream()
.map(pointGoodMapper::toDto) .map(pointGoodMapper::toDto)
@@ -40,10 +31,6 @@ public class PointMallController {
} }
@PostMapping("/redeem") @PostMapping("/redeem")
@Operation(summary = "Redeem good", description = "Redeem a point good")
@ApiResponse(responseCode = "200", description = "Remaining points",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
@SecurityRequirement(name = "JWT")
public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) { public Map<String, Integer> redeem(@RequestBody PointRedeemRequest req, Authentication auth) {
User user = userService.findByIdentifier(auth.getName()).orElseThrow(); User user = userService.findByIdentifier(auth.getName()).orElseThrow();
int point = pointMallService.redeem(user, req.getGoodId(), req.getContact()); int point = pointMallService.redeem(user, req.getGoodId(), req.getContact());

View File

@@ -1,33 +0,0 @@
package com.openisle.controller;
import com.openisle.dto.PostChangeLogDto;
import com.openisle.mapper.PostChangeLogMapper;
import com.openisle.service.PostChangeLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.List;
import java.util.stream.Collectors;
@RestController
@RequestMapping("/api/posts")
@RequiredArgsConstructor
public class PostChangeLogController {
private final PostChangeLogService changeLogService;
private final PostChangeLogMapper mapper;
@GetMapping("/{id}/change-logs")
@Operation(summary = "Post change logs", description = "List change logs for a post")
@ApiResponse(responseCode = "200", description = "Change logs",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostChangeLogDto.class))))
public List<PostChangeLogDto> listLogs(@PathVariable Long id) {
return changeLogService.listLogs(id).stream()
.map(mapper::toDto)
.collect(Collectors.toList());
}
}

View File

@@ -7,12 +7,6 @@ import com.openisle.dto.PollDto;
import com.openisle.mapper.PostMapper; import com.openisle.mapper.PostMapper;
import com.openisle.model.Post; import com.openisle.model.Post;
import com.openisle.service.*; import com.openisle.service.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -41,10 +35,6 @@ public class PostController {
private boolean postCaptchaEnabled; private boolean postCaptchaEnabled;
@PostMapping @PostMapping
@SecurityRequirement(name = "JWT")
@Operation(summary = "Create post", description = "Create a new post")
@ApiResponse(responseCode = "200", description = "Created post",
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
public ResponseEntity<PostDetailDto> createPost(@RequestBody PostRequest req, Authentication auth) { public ResponseEntity<PostDetailDto> createPost(@RequestBody PostRequest req, Authentication auth) {
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().build(); return ResponseEntity.badRequest().build();
@@ -63,10 +53,6 @@ public class PostController {
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Update post", description = "Update an existing post")
@ApiResponse(responseCode = "200", description = "Updated post",
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
public ResponseEntity<PostDetailDto> updatePost(@PathVariable Long id, @RequestBody PostRequest req, public ResponseEntity<PostDetailDto> updatePost(@PathVariable Long id, @RequestBody PostRequest req,
Authentication auth) { Authentication auth) {
Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(), Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(),
@@ -75,35 +61,21 @@ public class PostController {
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Delete post", description = "Delete a post")
@ApiResponse(responseCode = "200", description = "Post deleted")
public void deletePost(@PathVariable Long id, Authentication auth) { public void deletePost(@PathVariable Long id, Authentication auth) {
postService.deletePost(id, auth.getName()); postService.deletePost(id, auth.getName());
} }
@PostMapping("/{id}/close") @PostMapping("/{id}/close")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Close post", description = "Close a post to prevent further replies")
@ApiResponse(responseCode = "200", description = "Closed post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto close(@PathVariable Long id, Authentication auth) { public PostSummaryDto close(@PathVariable Long id, Authentication auth) {
return postMapper.toSummaryDto(postService.closePost(id, auth.getName())); return postMapper.toSummaryDto(postService.closePost(id, auth.getName()));
} }
@PostMapping("/{id}/reopen") @PostMapping("/{id}/reopen")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Reopen post", description = "Reopen a closed post")
@ApiResponse(responseCode = "200", description = "Reopened post",
content = @Content(schema = @Schema(implementation = PostSummaryDto.class)))
public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) { public PostSummaryDto reopen(@PathVariable Long id, Authentication auth) {
return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName())); return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName()));
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "Get post", description = "Get post details by id")
@ApiResponse(responseCode = "200", description = "Post detail",
content = @Content(schema = @Schema(implementation = PostDetailDto.class)))
public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) { public ResponseEntity<PostDetailDto> getPost(@PathVariable Long id, Authentication auth) {
String viewer = auth != null ? auth.getName() : null; String viewer = auth != null ? auth.getName() : null;
Post post = postService.viewPost(id, viewer); Post post = postService.viewPost(id, viewer);
@@ -111,35 +83,23 @@ public class PostController {
} }
@PostMapping("/{id}/lottery/join") @PostMapping("/{id}/lottery/join")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Join lottery", description = "Join a lottery for the post")
@ApiResponse(responseCode = "200", description = "Joined lottery")
public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) { public ResponseEntity<Void> joinLottery(@PathVariable Long id, Authentication auth) {
postService.joinLottery(id, auth.getName()); postService.joinLottery(id, auth.getName());
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@GetMapping("/{id}/poll/progress") @GetMapping("/{id}/poll/progress")
@Operation(summary = "Poll progress", description = "Get poll progress for a post")
@ApiResponse(responseCode = "200", description = "Poll progress",
content = @Content(schema = @Schema(implementation = PollDto.class)))
public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) { public ResponseEntity<PollDto> pollProgress(@PathVariable Long id) {
return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll()); return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll());
} }
@PostMapping("/{id}/poll/vote") @PostMapping("/{id}/poll/vote")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Vote poll", description = "Vote on a poll option")
@ApiResponse(responseCode = "200", description = "Vote recorded")
public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") List<Integer> option, Authentication auth) { public ResponseEntity<Void> vote(@PathVariable Long id, @RequestParam("option") List<Integer> option, Authentication auth) {
postService.votePoll(id, auth.getName(), option); postService.votePoll(id, auth.getName(), option);
return ResponseEntity.ok().build(); return ResponseEntity.ok().build();
} }
@GetMapping @GetMapping
@Operation(summary = "List posts", description = "List posts by various filters")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, public List<PostSummaryDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds, @RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagId", required = false) Long tagId,
@@ -177,9 +137,6 @@ public class PostController {
} }
@GetMapping("/ranking") @GetMapping("/ranking")
@Operation(summary = "Ranking posts", description = "List posts by view rankings")
@ApiResponse(responseCode = "200", description = "Ranked posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, public List<PostSummaryDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds, @RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagId", required = false) Long tagId,
@@ -205,9 +162,6 @@ public class PostController {
} }
@GetMapping("/latest-reply") @GetMapping("/latest-reply")
@Operation(summary = "Latest reply posts", description = "List posts by latest replies")
@ApiResponse(responseCode = "200", description = "Posts sorted by latest reply",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, public List<PostSummaryDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds, @RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagId", required = false) Long tagId,
@@ -233,9 +187,6 @@ public class PostController {
} }
@GetMapping("/featured") @GetMapping("/featured")
@Operation(summary = "Featured posts", description = "List featured posts")
@ApiResponse(responseCode = "200", description = "Featured posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, public List<PostSummaryDto> featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds, @RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
@RequestParam(value = "tagId", required = false) Long tagId, @RequestParam(value = "tagId", required = false) Long tagId,

View File

@@ -7,11 +7,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@RestController @RestController
@RequestMapping("/api/push") @RequestMapping("/api/push")
@@ -22,9 +17,6 @@ public class PushSubscriptionController {
private String publicKey; private String publicKey;
@GetMapping("/public-key") @GetMapping("/public-key")
@Operation(summary = "Get public key", description = "Retrieve web push public key")
@ApiResponse(responseCode = "200", description = "Public key",
content = @Content(schema = @Schema(implementation = PushPublicKeyDto.class)))
public PushPublicKeyDto getPublicKey() { public PushPublicKeyDto getPublicKey() {
PushPublicKeyDto r = new PushPublicKeyDto(); PushPublicKeyDto r = new PushPublicKeyDto();
r.setKey(publicKey); r.setKey(publicKey);
@@ -32,9 +24,6 @@ public class PushSubscriptionController {
} }
@PostMapping("/subscribe") @PostMapping("/subscribe")
@Operation(summary = "Subscribe", description = "Subscribe to push notifications")
@ApiResponse(responseCode = "200", description = "Subscribed")
@SecurityRequirement(name = "JWT")
public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) { public void subscribe(@RequestBody PushSubscriptionRequest req, Authentication auth) {
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth()); pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
} }

View File

@@ -12,11 +12,6 @@ import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@@ -31,18 +26,11 @@ public class ReactionController {
* Get all available reaction types. * Get all available reaction types.
*/ */
@GetMapping("/reaction-types") @GetMapping("/reaction-types")
@Operation(summary = "List reaction types", description = "Get all available reaction types")
@ApiResponse(responseCode = "200", description = "Reaction types",
content = @Content(schema = @Schema(implementation = ReactionType[].class)))
public ReactionType[] listReactionTypes() { public ReactionType[] listReactionTypes() {
return ReactionType.values(); return ReactionType.values();
} }
@PostMapping("/posts/{postId}/reactions") @PostMapping("/posts/{postId}/reactions")
@Operation(summary = "React to post", description = "React to a post")
@ApiResponse(responseCode = "200", description = "Reaction result",
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId, public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId,
@RequestBody ReactionRequest req, @RequestBody ReactionRequest req,
Authentication auth) { Authentication auth) {
@@ -58,10 +46,6 @@ public class ReactionController {
} }
@PostMapping("/comments/{commentId}/reactions") @PostMapping("/comments/{commentId}/reactions")
@Operation(summary = "React to comment", description = "React to a comment")
@ApiResponse(responseCode = "200", description = "Reaction result",
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId, public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId,
@RequestBody ReactionRequest req, @RequestBody ReactionRequest req,
Authentication auth) { Authentication auth) {
@@ -77,10 +61,6 @@ public class ReactionController {
} }
@PostMapping("/messages/{messageId}/reactions") @PostMapping("/messages/{messageId}/reactions")
@Operation(summary = "React to message", description = "React to a message")
@ApiResponse(responseCode = "200", description = "Reaction result",
content = @Content(schema = @Schema(implementation = ReactionDto.class)))
@SecurityRequirement(name = "JWT")
public ResponseEntity<ReactionDto> reactToMessage(@PathVariable Long messageId, public ResponseEntity<ReactionDto> reactToMessage(@PathVariable Long messageId,
@RequestBody ReactionRequest req, @RequestBody ReactionRequest req,
Authentication auth) { Authentication auth) {

View File

@@ -13,10 +13,6 @@ import org.jsoup.safety.Safelist;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import com.vladsch.flexmark.ext.autolink.AutolinkExtension; import com.vladsch.flexmark.ext.autolink.AutolinkExtension;
import com.vladsch.flexmark.ext.tables.TablesExtension; import com.vladsch.flexmark.ext.tables.TablesExtension;
@@ -67,8 +63,6 @@ public class RssController {
} }
@GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8") @GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8")
@Operation(summary = "RSS feed", description = "Generate RSS feed for latest posts")
@ApiResponse(responseCode = "200", description = "RSS XML", content = @Content(schema = @Schema(implementation = String.class)))
public String feed() { public String feed() {
// 建议 20你现在是 10这里保留你的 10 // 建议 20你现在是 10这里保留你的 10
List<Post> posts = postService.listLatestRssPosts(10); List<Post> posts = postService.listLatestRssPosts(10);

View File

@@ -11,11 +11,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.List; import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
@@ -29,9 +24,6 @@ public class SearchController {
private final PostMapper postMapper; private final PostMapper postMapper;
@GetMapping("/users") @GetMapping("/users")
@Operation(summary = "Search users", description = "Search users by keyword")
@ApiResponse(responseCode = "200", description = "List of users",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
public List<UserDto> searchUsers(@RequestParam String keyword) { public List<UserDto> searchUsers(@RequestParam String keyword) {
return searchService.searchUsers(keyword).stream() return searchService.searchUsers(keyword).stream()
.map(userMapper::toDto) .map(userMapper::toDto)
@@ -39,9 +31,6 @@ public class SearchController {
} }
@GetMapping("/posts") @GetMapping("/posts")
@Operation(summary = "Search posts", description = "Search posts by keyword")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> searchPosts(@RequestParam String keyword) { public List<PostSummaryDto> searchPosts(@RequestParam String keyword) {
return searchService.searchPosts(keyword).stream() return searchService.searchPosts(keyword).stream()
.map(postMapper::toSummaryDto) .map(postMapper::toSummaryDto)
@@ -49,9 +38,6 @@ public class SearchController {
} }
@GetMapping("/posts/content") @GetMapping("/posts/content")
@Operation(summary = "Search posts by content", description = "Search posts by content keyword")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) { public List<PostSummaryDto> searchPostsByContent(@RequestParam String keyword) {
return searchService.searchPostsByContent(keyword).stream() return searchService.searchPostsByContent(keyword).stream()
.map(postMapper::toSummaryDto) .map(postMapper::toSummaryDto)
@@ -59,9 +45,6 @@ public class SearchController {
} }
@GetMapping("/posts/title") @GetMapping("/posts/title")
@Operation(summary = "Search posts by title", description = "Search posts by title keyword")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) { public List<PostSummaryDto> searchPostsByTitle(@RequestParam String keyword) {
return searchService.searchPostsByTitle(keyword).stream() return searchService.searchPostsByTitle(keyword).stream()
.map(postMapper::toSummaryDto) .map(postMapper::toSummaryDto)
@@ -69,9 +52,6 @@ public class SearchController {
} }
@GetMapping("/global") @GetMapping("/global")
@Operation(summary = "Global search", description = "Search users and posts globally")
@ApiResponse(responseCode = "200", description = "Search results",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = SearchResultDto.class))))
public List<SearchResultDto> global(@RequestParam String keyword) { public List<SearchResultDto> global(@RequestParam String keyword) {
return searchService.globalSearch(keyword).stream() return searchService.globalSearch(keyword).stream()
.map(r -> { .map(r -> {

View File

@@ -10,10 +10,6 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.util.List; import java.util.List;
@@ -30,9 +26,6 @@ public class SitemapController {
private String websiteUrl; private String websiteUrl;
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE) @GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
@Operation(summary = "Sitemap", description = "Generate sitemap xml")
@ApiResponse(responseCode = "200", description = "Sitemap xml",
content = @Content(schema = @Schema(implementation = String.class)))
public ResponseEntity<String> sitemap() { public ResponseEntity<String> sitemap() {
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED); List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);

View File

@@ -8,11 +8,6 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
@@ -26,9 +21,6 @@ public class StatController {
private final StatService statService; private final StatService statService;
@GetMapping("/dau") @GetMapping("/dau")
@Operation(summary = "Daily active users", description = "Get daily active user count")
@ApiResponse(responseCode = "200", description = "DAU count",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
public Map<String, Long> dau(@RequestParam(value = "date", required = false) public Map<String, Long> dau(@RequestParam(value = "date", required = false)
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
long count = userVisitService.countDau(date); long count = userVisitService.countDau(date);
@@ -36,9 +28,6 @@ public class StatController {
} }
@GetMapping("/dau-range") @GetMapping("/dau-range")
@Operation(summary = "DAU range", description = "Get daily active users over range of days")
@ApiResponse(responseCode = "200", description = "DAU data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) { public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1; if (days < 1) days = 1;
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();
@@ -53,9 +42,6 @@ public class StatController {
} }
@GetMapping("/new-users-range") @GetMapping("/new-users-range")
@Operation(summary = "New users range", description = "Get new users over range of days")
@ApiResponse(responseCode = "200", description = "New user data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
public List<Map<String, Object>> newUsersRange(@RequestParam(value = "days", defaultValue = "30") int days) { public List<Map<String, Object>> newUsersRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1; if (days < 1) days = 1;
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();
@@ -70,9 +56,6 @@ public class StatController {
} }
@GetMapping("/posts-range") @GetMapping("/posts-range")
@Operation(summary = "Posts range", description = "Get posts count over range of days")
@ApiResponse(responseCode = "200", description = "Post data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
public List<Map<String, Object>> postsRange(@RequestParam(value = "days", defaultValue = "30") int days) { public List<Map<String, Object>> postsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1; if (days < 1) days = 1;
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();
@@ -87,9 +70,6 @@ public class StatController {
} }
@GetMapping("/comments-range") @GetMapping("/comments-range")
@Operation(summary = "Comments range", description = "Get comments count over range of days")
@ApiResponse(responseCode = "200", description = "Comment data",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = java.util.Map.class))))
public List<Map<String, Object>> commentsRange(@RequestParam(value = "days", defaultValue = "30") int days) { public List<Map<String, Object>> commentsRange(@RequestParam(value = "days", defaultValue = "30") int days) {
if (days < 1) days = 1; if (days < 1) days = 1;
LocalDate end = LocalDate.now(); LocalDate end = LocalDate.now();

View File

@@ -4,9 +4,6 @@ import com.openisle.service.SubscriptionService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication; import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
/** Endpoints for subscribing to posts, comments and users. */ /** Endpoints for subscribing to posts, comments and users. */
@RestController @RestController
@@ -16,49 +13,31 @@ public class SubscriptionController {
private final SubscriptionService subscriptionService; private final SubscriptionService subscriptionService;
@PostMapping("/posts/{postId}") @PostMapping("/posts/{postId}")
@Operation(summary = "Subscribe post", description = "Subscribe to a post")
@ApiResponse(responseCode = "200", description = "Subscribed")
@SecurityRequirement(name = "JWT")
public void subscribePost(@PathVariable Long postId, Authentication auth) { public void subscribePost(@PathVariable Long postId, Authentication auth) {
subscriptionService.subscribePost(auth.getName(), postId); subscriptionService.subscribePost(auth.getName(), postId);
} }
@DeleteMapping("/posts/{postId}") @DeleteMapping("/posts/{postId}")
@Operation(summary = "Unsubscribe post", description = "Unsubscribe from a post")
@ApiResponse(responseCode = "200", description = "Unsubscribed")
@SecurityRequirement(name = "JWT")
public void unsubscribePost(@PathVariable Long postId, Authentication auth) { public void unsubscribePost(@PathVariable Long postId, Authentication auth) {
subscriptionService.unsubscribePost(auth.getName(), postId); subscriptionService.unsubscribePost(auth.getName(), postId);
} }
@PostMapping("/comments/{commentId}") @PostMapping("/comments/{commentId}")
@Operation(summary = "Subscribe comment", description = "Subscribe to a comment")
@ApiResponse(responseCode = "200", description = "Subscribed")
@SecurityRequirement(name = "JWT")
public void subscribeComment(@PathVariable Long commentId, Authentication auth) { public void subscribeComment(@PathVariable Long commentId, Authentication auth) {
subscriptionService.subscribeComment(auth.getName(), commentId); subscriptionService.subscribeComment(auth.getName(), commentId);
} }
@DeleteMapping("/comments/{commentId}") @DeleteMapping("/comments/{commentId}")
@Operation(summary = "Unsubscribe comment", description = "Unsubscribe from a comment")
@ApiResponse(responseCode = "200", description = "Unsubscribed")
@SecurityRequirement(name = "JWT")
public void unsubscribeComment(@PathVariable Long commentId, Authentication auth) { public void unsubscribeComment(@PathVariable Long commentId, Authentication auth) {
subscriptionService.unsubscribeComment(auth.getName(), commentId); subscriptionService.unsubscribeComment(auth.getName(), commentId);
} }
@PostMapping("/users/{username}") @PostMapping("/users/{username}")
@Operation(summary = "Subscribe user", description = "Subscribe to a user")
@ApiResponse(responseCode = "200", description = "Subscribed")
@SecurityRequirement(name = "JWT")
public void subscribeUser(@PathVariable String username, Authentication auth) { public void subscribeUser(@PathVariable String username, Authentication auth) {
subscriptionService.subscribeUser(auth.getName(), username); subscriptionService.subscribeUser(auth.getName(), username);
} }
@DeleteMapping("/users/{username}") @DeleteMapping("/users/{username}")
@Operation(summary = "Unsubscribe user", description = "Unsubscribe from a user")
@ApiResponse(responseCode = "200", description = "Unsubscribed")
@SecurityRequirement(name = "JWT")
public void unsubscribeUser(@PathVariable String username, Authentication auth) { public void unsubscribeUser(@PathVariable String username, Authentication auth) {
subscriptionService.unsubscribeUser(auth.getName(), username); subscriptionService.unsubscribeUser(auth.getName(), username);
} }

View File

@@ -13,12 +13,6 @@ import com.openisle.service.PostService;
import com.openisle.service.TagService; import com.openisle.service.TagService;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@@ -35,10 +29,6 @@ public class TagController {
private final TagMapper tagMapper; private final TagMapper tagMapper;
@PostMapping @PostMapping
@Operation(summary = "Create tag", description = "Create a new tag")
@ApiResponse(responseCode = "200", description = "Created tag",
content = @Content(schema = @Schema(implementation = TagDto.class)))
@SecurityRequirement(name = "JWT")
public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) { public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
boolean approved = true; boolean approved = true;
if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) { if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
@@ -59,9 +49,6 @@ public class TagController {
} }
@PutMapping("/{id}") @PutMapping("/{id}")
@Operation(summary = "Update tag", description = "Update an existing tag")
@ApiResponse(responseCode = "200", description = "Updated tag",
content = @Content(schema = @Schema(implementation = TagDto.class)))
public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) { public TagDto update(@PathVariable Long id, @RequestBody TagRequest req) {
Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
long count = postService.countPostsByTag(tag.getId()); long count = postService.countPostsByTag(tag.getId());
@@ -69,16 +56,11 @@ public class TagController {
} }
@DeleteMapping("/{id}") @DeleteMapping("/{id}")
@Operation(summary = "Delete tag", description = "Delete a tag by id")
@ApiResponse(responseCode = "200", description = "Tag deleted")
public void delete(@PathVariable Long id) { public void delete(@PathVariable Long id) {
tagService.deleteTag(id); tagService.deleteTag(id);
} }
@GetMapping @GetMapping
@Operation(summary = "List tags", description = "List tags with optional keyword")
@ApiResponse(responseCode = "200", description = "List of tags",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword, public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
List<Tag> tags = tagService.searchTags(keyword); List<Tag> tags = tagService.searchTags(keyword);
@@ -95,9 +77,6 @@ public class TagController {
} }
@GetMapping("/{id}") @GetMapping("/{id}")
@Operation(summary = "Get tag", description = "Get tag by id")
@ApiResponse(responseCode = "200", description = "Tag detail",
content = @Content(schema = @Schema(implementation = TagDto.class)))
public TagDto get(@PathVariable Long id) { public TagDto get(@PathVariable Long id) {
Tag tag = tagService.getTag(id); Tag tag = tagService.getTag(id);
long count = postService.countPostsByTag(tag.getId()); long count = postService.countPostsByTag(tag.getId());
@@ -105,9 +84,6 @@ public class TagController {
} }
@GetMapping("/{id}/posts") @GetMapping("/{id}/posts")
@Operation(summary = "List posts by tag", description = "Get posts with specific tag")
@ApiResponse(responseCode = "200", description = "List of posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostSummaryDto.class))))
public List<PostSummaryDto> listPostsByTag(@PathVariable Long id, public List<PostSummaryDto> listPostsByTag(@PathVariable Long id,
@RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "page", required = false) Integer page,
@RequestParam(value = "pageSize", required = false) Integer pageSize) { @RequestParam(value = "pageSize", required = false) Integer pageSize) {

View File

@@ -6,10 +6,6 @@ import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import java.io.ByteArrayInputStream; import java.io.ByteArrayInputStream;
import java.io.IOException; import java.io.IOException;
@@ -31,9 +27,6 @@ public class UploadController {
private long maxUploadSize; private long maxUploadSize;
@PostMapping @PostMapping
@Operation(summary = "Upload file", description = "Upload image file")
@ApiResponse(responseCode = "200", description = "Upload result",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) { public ResponseEntity<?> upload(@RequestParam("file") MultipartFile file) {
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) { if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image")); return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
@@ -55,9 +48,6 @@ public class UploadController {
} }
@PostMapping("/url") @PostMapping("/url")
@Operation(summary = "Upload from URL", description = "Upload image from remote URL")
@ApiResponse(responseCode = "200", description = "Upload result",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) { public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) {
String link = body.get("url"); String link = body.get("url");
if (link == null || link.isBlank()) { if (link == null || link.isBlank()) {
@@ -86,9 +76,6 @@ public class UploadController {
} }
@GetMapping("/presign") @GetMapping("/presign")
@Operation(summary = "Presign upload", description = "Get presigned upload URL")
@ApiResponse(responseCode = "200", description = "Presigned URL",
content = @Content(schema = @Schema(implementation = java.util.Map.class)))
public java.util.Map<String, String> presign(@RequestParam("filename") String filename) { public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
return imageUploader.presignUpload(filename); return imageUploader.presignUpload(filename);
} }

View File

@@ -6,12 +6,6 @@ import com.openisle.mapper.TagMapper;
import com.openisle.mapper.UserMapper; import com.openisle.mapper.UserMapper;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.service.*; import com.openisle.service.*;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -54,20 +48,12 @@ public class UserController {
private int defaultTagsLimit; private int defaultTagsLimit;
@GetMapping("/me") @GetMapping("/me")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Current user", description = "Get current authenticated user information")
@ApiResponse(responseCode = "200", description = "User detail",
content = @Content(schema = @Schema(implementation = UserDto.class)))
public ResponseEntity<UserDto> me(Authentication auth) { public ResponseEntity<UserDto> me(Authentication auth) {
User user = userService.findByUsername(auth.getName()).orElseThrow(); User user = userService.findByUsername(auth.getName()).orElseThrow();
return ResponseEntity.ok(userMapper.toDto(user, auth)); return ResponseEntity.ok(userMapper.toDto(user, auth));
} }
@PostMapping("/me/avatar") @PostMapping("/me/avatar")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Upload avatar", description = "Upload avatar for current user")
@ApiResponse(responseCode = "200", description = "Upload result",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file, public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file,
Authentication auth) { Authentication auth) {
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) { if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
@@ -87,10 +73,6 @@ public class UserController {
} }
@PutMapping("/me") @PutMapping("/me")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Update profile", description = "Update current user's profile")
@ApiResponse(responseCode = "200", description = "Updated profile",
content = @Content(schema = @Schema(implementation = Map.class)))
public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto, public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto,
Authentication auth) { Authentication auth) {
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction()); User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
@@ -101,19 +83,12 @@ public class UserController {
} }
@PostMapping("/me/signin") @PostMapping("/me/signin")
@SecurityRequirement(name = "JWT")
@Operation(summary = "Daily sign in", description = "Sign in to receive rewards")
@ApiResponse(responseCode = "200", description = "Sign in reward",
content = @Content(schema = @Schema(implementation = Map.class)))
public Map<String, Integer> signIn(Authentication auth) { public Map<String, Integer> signIn(Authentication auth) {
int reward = levelService.awardForSignin(auth.getName()); int reward = levelService.awardForSignin(auth.getName());
return Map.of("reward", reward); return Map.of("reward", reward);
} }
@GetMapping("/{identifier}") @GetMapping("/{identifier}")
@Operation(summary = "Get user", description = "Get user by identifier")
@ApiResponse(responseCode = "200", description = "User detail",
content = @Content(schema = @Schema(implementation = UserDto.class)))
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier, public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
Authentication auth) { Authentication auth) {
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found")); User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
@@ -121,9 +96,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/posts") @GetMapping("/{identifier}/posts")
@Operation(summary = "User posts", description = "Get recent posts by user")
@ApiResponse(responseCode = "200", description = "User posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
public java.util.List<PostMetaDto> userPosts(@PathVariable("identifier") String identifier, public java.util.List<PostMetaDto> userPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultPostsLimit; int l = limit != null ? limit : defaultPostsLimit;
@@ -134,9 +106,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/subscribed-posts") @GetMapping("/{identifier}/subscribed-posts")
@Operation(summary = "Subscribed posts", description = "Get posts the user subscribed to")
@ApiResponse(responseCode = "200", description = "Subscribed posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
public java.util.List<PostMetaDto> subscribedPosts(@PathVariable("identifier") String identifier, public java.util.List<PostMetaDto> subscribedPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultPostsLimit; int l = limit != null ? limit : defaultPostsLimit;
@@ -148,9 +117,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/replies") @GetMapping("/{identifier}/replies")
@Operation(summary = "User replies", description = "Get recent replies by user")
@ApiResponse(responseCode = "200", description = "User replies",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))))
public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier, public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultRepliesLimit; int l = limit != null ? limit : defaultRepliesLimit;
@@ -161,9 +127,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/hot-posts") @GetMapping("/{identifier}/hot-posts")
@Operation(summary = "User hot posts", description = "Get most reacted posts by user")
@ApiResponse(responseCode = "200", description = "Hot posts",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = PostMetaDto.class))))
public java.util.List<PostMetaDto> hotPosts(@PathVariable("identifier") String identifier, public java.util.List<PostMetaDto> hotPosts(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10; int l = limit != null ? limit : 10;
@@ -175,9 +138,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/hot-replies") @GetMapping("/{identifier}/hot-replies")
@Operation(summary = "User hot replies", description = "Get most reacted replies by user")
@ApiResponse(responseCode = "200", description = "Hot replies",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = CommentInfoDto.class))))
public java.util.List<CommentInfoDto> hotReplies(@PathVariable("identifier") String identifier, public java.util.List<CommentInfoDto> hotReplies(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10; int l = limit != null ? limit : 10;
@@ -189,9 +149,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/hot-tags") @GetMapping("/{identifier}/hot-tags")
@Operation(summary = "User hot tags", description = "Get tags frequently used by user")
@ApiResponse(responseCode = "200", description = "Hot tags",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
public java.util.List<TagDto> hotTags(@PathVariable("identifier") String identifier, public java.util.List<TagDto> hotTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : 10; int l = limit != null ? limit : 10;
@@ -204,9 +161,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/tags") @GetMapping("/{identifier}/tags")
@Operation(summary = "User tags", description = "Get recent tags used by user")
@ApiResponse(responseCode = "200", description = "User tags",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = TagDto.class))))
public java.util.List<TagDto> userTags(@PathVariable("identifier") String identifier, public java.util.List<TagDto> userTags(@PathVariable("identifier") String identifier,
@RequestParam(value = "limit", required = false) Integer limit) { @RequestParam(value = "limit", required = false) Integer limit) {
int l = limit != null ? limit : defaultTagsLimit; int l = limit != null ? limit : defaultTagsLimit;
@@ -217,9 +171,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/following") @GetMapping("/{identifier}/following")
@Operation(summary = "Following users", description = "Get users that this user is following")
@ApiResponse(responseCode = "200", description = "Following list",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) { public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
User user = userService.findByIdentifier(identifier).orElseThrow(); User user = userService.findByIdentifier(identifier).orElseThrow();
return subscriptionService.getSubscribedUsers(user.getUsername()).stream() return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
@@ -228,9 +179,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/followers") @GetMapping("/{identifier}/followers")
@Operation(summary = "Followers", description = "Get followers of this user")
@ApiResponse(responseCode = "200", description = "Followers list",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) { public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
User user = userService.findByIdentifier(identifier).orElseThrow(); User user = userService.findByIdentifier(identifier).orElseThrow();
return subscriptionService.getSubscribers(user.getUsername()).stream() return subscriptionService.getSubscribers(user.getUsername()).stream()
@@ -239,9 +187,6 @@ public class UserController {
} }
@GetMapping("/admins") @GetMapping("/admins")
@Operation(summary = "Admin users", description = "List administrator users")
@ApiResponse(responseCode = "200", description = "Admin users",
content = @Content(array = @ArraySchema(schema = @Schema(implementation = UserDto.class))))
public java.util.List<UserDto> admins() { public java.util.List<UserDto> admins() {
return userService.getAdmins().stream() return userService.getAdmins().stream()
.map(userMapper::toDto) .map(userMapper::toDto)
@@ -249,9 +194,6 @@ public class UserController {
} }
@GetMapping("/{identifier}/all") @GetMapping("/{identifier}/all")
@Operation(summary = "User aggregate", description = "Get aggregate information for user")
@ApiResponse(responseCode = "200", description = "User aggregate",
content = @Content(schema = @Schema(implementation = UserAggregateDto.class)))
public ResponseEntity<UserAggregateDto> userAggregate(@PathVariable("identifier") String identifier, public ResponseEntity<UserAggregateDto> userAggregate(@PathVariable("identifier") String identifier,
@RequestParam(value = "postsLimit", required = false) Integer postsLimit, @RequestParam(value = "postsLimit", required = false) Integer postsLimit,
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit, @RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,

View File

@@ -1,32 +0,0 @@
package com.openisle.dto;
import com.openisle.model.PostChangeType;
import lombok.Getter;
import lombok.Setter;
import java.time.LocalDateTime;
import java.util.List;
@Getter
@Setter
public class PostChangeLogDto {
private Long id;
private String username;
private String userAvatar;
private PostChangeType type;
private LocalDateTime time;
private String oldTitle;
private String newTitle;
private String oldContent;
private String newContent;
private CategoryDto oldCategory;
private CategoryDto newCategory;
private List<TagDto> oldTags;
private List<TagDto> newTags;
private Boolean oldClosed;
private Boolean newClosed;
private LocalDateTime oldPinnedAt;
private LocalDateTime newPinnedAt;
private Boolean oldFeatured;
private Boolean newFeatured;
}

View File

@@ -1,92 +0,0 @@
package com.openisle.mapper;
import com.openisle.dto.CategoryDto;
import com.openisle.dto.PostChangeLogDto;
import com.openisle.dto.TagDto;
import com.openisle.model.*;
import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
public class PostChangeLogMapper {
private final CategoryRepository categoryRepository;
private final TagRepository tagRepository;
private final CategoryMapper categoryMapper;
private final TagMapper tagMapper;
public PostChangeLogDto toDto(PostChangeLog log) {
PostChangeLogDto dto = new PostChangeLogDto();
dto.setId(log.getId());
if (log.getUser() != null) {
dto.setUsername(log.getUser().getUsername());
dto.setUserAvatar(log.getUser().getAvatar());
}
dto.setType(log.getType());
dto.setTime(log.getCreatedAt());
if (log instanceof PostTitleChangeLog t) {
dto.setOldTitle(t.getOldTitle());
dto.setNewTitle(t.getNewTitle());
} else if (log instanceof PostContentChangeLog c) {
dto.setOldContent(c.getOldContent());
dto.setNewContent(c.getNewContent());
} else if (log instanceof PostCategoryChangeLog cat) {
dto.setOldCategory(mapCategory(cat.getOldCategory()));
dto.setNewCategory(mapCategory(cat.getNewCategory()));
} else if (log instanceof PostTagChangeLog tag) {
dto.setOldTags(mapTags(tag.getOldTags()));
dto.setNewTags(mapTags(tag.getNewTags()));
} else if (log instanceof PostClosedChangeLog cl) {
dto.setOldClosed(cl.isOldClosed());
dto.setNewClosed(cl.isNewClosed());
} else if (log instanceof PostPinnedChangeLog p) {
dto.setOldPinnedAt(p.getOldPinnedAt());
dto.setNewPinnedAt(p.getNewPinnedAt());
} else if (log instanceof PostFeaturedChangeLog f) {
dto.setOldFeatured(f.isOldFeatured());
dto.setNewFeatured(f.isNewFeatured());
}
return dto;
}
private CategoryDto mapCategory(String name) {
if (name == null) {
return null;
}
return categoryRepository.findByName(name)
.map(categoryMapper::toDto)
.orElseGet(() -> {
CategoryDto dto = new CategoryDto();
dto.setName(name);
return dto;
});
}
private List<TagDto> mapTags(String tags) {
if (tags == null || tags.isBlank()) {
return Collections.emptyList();
}
return Arrays.stream(tags.split(","))
.map(String::trim)
.map(this::mapTag)
.collect(Collectors.toList());
}
private TagDto mapTag(String name) {
return tagRepository.findByName(name)
.map(tagMapper::toDto)
.orElseGet(() -> {
TagDto dto = new TagDto();
dto.setName(name);
return dto;
});
}
}

View File

@@ -1,17 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_category_change_logs")
public class PostCategoryChangeLog extends PostChangeLog {
private String oldCategory;
private String newCategory;
}

View File

@@ -1,37 +0,0 @@
package com.openisle.model;
import jakarta.persistence.*;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.hibernate.annotations.CreationTimestamp;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_change_logs")
@Inheritance(strategy = InheritanceType.JOINED)
public abstract class PostChangeLog {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "post_id")
private Post post;
@ManyToOne(fetch = FetchType.LAZY, optional = true)
@JoinColumn(name = "user_id")
private User user;
@CreationTimestamp
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
@Enumerated(EnumType.STRING)
@Column(nullable = false)
private PostChangeType type;
}

View File

@@ -1,13 +0,0 @@
package com.openisle.model;
public enum PostChangeType {
CONTENT,
TITLE,
CATEGORY,
TAG,
CLOSED,
PINNED,
FEATURED,
VOTE_RESULT,
LOTTERY_RESULT
}

View File

@@ -1,17 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_closed_change_logs")
public class PostClosedChangeLog extends PostChangeLog {
private boolean oldClosed;
private boolean newClosed;
}

View File

@@ -1,21 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_content_change_logs")
public class PostContentChangeLog extends PostChangeLog {
@Column(name = "old_content", columnDefinition = "LONGTEXT")
private String oldContent;
@Column(name = "new_content", columnDefinition = "LONGTEXT")
private String newContent;
}

View File

@@ -1,17 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_featured_change_logs")
public class PostFeaturedChangeLog extends PostChangeLog {
private boolean oldFeatured;
private boolean newFeatured;
}

View File

@@ -1,16 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_lottery_result_change_logs")
public class PostLotteryResultChangeLog extends PostChangeLog {
}

View File

@@ -1,19 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import java.time.LocalDateTime;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_pinned_change_logs")
public class PostPinnedChangeLog extends PostChangeLog {
private LocalDateTime oldPinnedAt;
private LocalDateTime newPinnedAt;
}

View File

@@ -1,21 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_tag_change_logs")
public class PostTagChangeLog extends PostChangeLog {
@Column(name = "old_tags")
private String oldTags;
@Column(name = "new_tags")
private String newTags;
}

View File

@@ -1,17 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_title_change_logs")
public class PostTitleChangeLog extends PostChangeLog {
private String oldTitle;
private String newTitle;
}

View File

@@ -1,16 +0,0 @@
package com.openisle.model;
import jakarta.persistence.Entity;
import jakarta.persistence.Table;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
@Getter
@Setter
@NoArgsConstructor
@Entity
@Table(name = "post_vote_result_change_logs")
public class PostVoteResultChangeLog extends PostChangeLog {
}

View File

@@ -4,10 +4,7 @@ import com.openisle.model.Category;
import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface CategoryRepository extends JpaRepository<Category, Long> { public interface CategoryRepository extends JpaRepository<Category, Long> {
List<Category> findByNameContainingIgnoreCase(String keyword); List<Category> findByNameContainingIgnoreCase(String keyword);
Optional<Category> findByName(String name);
} }

View File

@@ -10,7 +10,6 @@ import java.util.List;
public interface PointHistoryRepository extends JpaRepository<PointHistory, Long> { public interface PointHistoryRepository extends JpaRepository<PointHistory, Long> {
List<PointHistory> findByUserOrderByIdDesc(User user); List<PointHistory> findByUserOrderByIdDesc(User user);
List<PointHistory> findByUserOrderByIdAsc(User user);
long countByUser(User user); long countByUser(User user);
List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt); List<PointHistory> findByUserAndCreatedAtAfterOrderByCreatedAtDesc(User user, LocalDateTime createdAt);

View File

@@ -1,11 +0,0 @@
package com.openisle.repository;
import com.openisle.model.Post;
import com.openisle.model.PostChangeLog;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.List;
public interface PostChangeLogRepository extends JpaRepository<PostChangeLog, Long> {
List<PostChangeLog> findByPostOrderByCreatedAtAsc(Post post);
}

View File

@@ -6,7 +6,6 @@ import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Pageable;
import java.util.List; import java.util.List;
import java.util.Optional;
public interface TagRepository extends JpaRepository<Tag, Long> { public interface TagRepository extends JpaRepository<Tag, Long> {
List<Tag> findByNameContainingIgnoreCase(String keyword); List<Tag> findByNameContainingIgnoreCase(String keyword);
@@ -16,6 +15,4 @@ public interface TagRepository extends JpaRepository<Tag, Long> {
List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable); List<Tag> findByCreatorOrderByCreatedAtDesc(User creator, Pageable pageable);
List<Tag> findByCreator(User creator); List<Tag> findByCreator(User creator);
Optional<Tag> findByName(String name);
} }

View File

@@ -225,20 +225,17 @@ public class PointService {
*/ */
public int recalculateUserPoints(User user) { public int recalculateUserPoints(User user) {
// 获取用户所有的积分历史记录(由于@Where注解已删除的记录会被自动过滤 // 获取用户所有的积分历史记录(由于@Where注解已删除的记录会被自动过滤
List<PointHistory> histories = pointHistoryRepository.findByUserOrderByIdAsc(user); List<PointHistory> histories = pointHistoryRepository.findByUserOrderByIdDesc(user);
int totalPoints = 0; int totalPoints = 0;
for (PointHistory history : histories) { for (PointHistory history : histories) {
totalPoints += history.getAmount(); totalPoints += history.getAmount();
// 重新计算每条历史记录的余额
history.setBalance(totalPoints);
} }
// 批量更新历史记录及用户积分 // 更新用户积分
pointHistoryRepository.saveAll(histories);
user.setPoint(totalPoints); user.setPoint(totalPoints);
userRepository.save(user); userRepository.save(user);
return totalPoints; return totalPoints;
} }

View File

@@ -1,117 +0,0 @@
package com.openisle.service;
import com.openisle.model.*;
import com.openisle.repository.PostChangeLogRepository;
import com.openisle.repository.PostRepository;
import com.openisle.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service
@RequiredArgsConstructor
public class PostChangeLogService {
private final PostChangeLogRepository logRepository;
private final PostRepository postRepository;
private final UserRepository userRepository;
private User getSystemUser() {
return userRepository.findByUsername("system")
.orElseThrow(() -> new IllegalStateException("System user not found"));
}
public void recordContentChange(Post post, User user, String oldContent, String newContent) {
PostContentChangeLog log = new PostContentChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CONTENT);
log.setOldContent(oldContent);
log.setNewContent(newContent);
logRepository.save(log);
}
public void recordTitleChange(Post post, User user, String oldTitle, String newTitle) {
PostTitleChangeLog log = new PostTitleChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.TITLE);
log.setOldTitle(oldTitle);
log.setNewTitle(newTitle);
logRepository.save(log);
}
public void recordCategoryChange(Post post, User user, String oldCategory, String newCategory) {
PostCategoryChangeLog log = new PostCategoryChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CATEGORY);
log.setOldCategory(oldCategory);
log.setNewCategory(newCategory);
logRepository.save(log);
}
public void recordTagChange(Post post, User user, Set<Tag> oldTags, Set<Tag> newTags) {
PostTagChangeLog log = new PostTagChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.TAG);
log.setOldTags(oldTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
log.setNewTags(newTags.stream().map(Tag::getName).collect(Collectors.joining(",")));
logRepository.save(log);
}
public void recordClosedChange(Post post, User user, boolean oldClosed, boolean newClosed) {
PostClosedChangeLog log = new PostClosedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.CLOSED);
log.setOldClosed(oldClosed);
log.setNewClosed(newClosed);
logRepository.save(log);
}
public void recordPinnedChange(Post post, User user, java.time.LocalDateTime oldPinnedAt, java.time.LocalDateTime newPinnedAt) {
PostPinnedChangeLog log = new PostPinnedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.PINNED);
log.setOldPinnedAt(oldPinnedAt);
log.setNewPinnedAt(newPinnedAt);
logRepository.save(log);
}
public void recordFeaturedChange(Post post, User user, boolean oldFeatured, boolean newFeatured) {
PostFeaturedChangeLog log = new PostFeaturedChangeLog();
log.setPost(post);
log.setUser(user);
log.setType(PostChangeType.FEATURED);
log.setOldFeatured(oldFeatured);
log.setNewFeatured(newFeatured);
logRepository.save(log);
}
public void recordVoteResult(Post post) {
PostVoteResultChangeLog log = new PostVoteResultChangeLog();
log.setPost(post);
log.setUser(getSystemUser());
log.setType(PostChangeType.VOTE_RESULT);
logRepository.save(log);
}
public void recordLotteryResult(Post post) {
PostLotteryResultChangeLog log = new PostLotteryResultChangeLog();
log.setPost(post);
log.setUser(getSystemUser());
log.setType(PostChangeType.LOTTERY_RESULT);
logRepository.save(log);
}
public List<PostChangeLog> listLogs(Long postId) {
Post post = postRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
return logRepository.findByPostOrderByCreatedAtAsc(post);
}
}

View File

@@ -1,6 +1,5 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.model.Post; import com.openisle.model.Post;
import com.openisle.model.PostStatus; import com.openisle.model.PostStatus;
import com.openisle.model.PostType; import com.openisle.model.PostType;
@@ -20,7 +19,6 @@ import com.openisle.repository.CategoryRepository;
import com.openisle.repository.TagRepository; import com.openisle.repository.TagRepository;
import com.openisle.service.SubscriptionService; import com.openisle.service.SubscriptionService;
import com.openisle.service.CommentService; import com.openisle.service.CommentService;
import com.openisle.service.PostChangeLogService;
import com.openisle.repository.CommentRepository; import com.openisle.repository.CommentRepository;
import com.openisle.repository.ReactionRepository; import com.openisle.repository.ReactionRepository;
import com.openisle.repository.PostSubscriptionRepository; import com.openisle.repository.PostSubscriptionRepository;
@@ -29,15 +27,12 @@ import com.openisle.repository.PollVoteRepository;
import com.openisle.model.Role; import com.openisle.model.Role;
import com.openisle.exception.RateLimitException; import com.openisle.exception.RateLimitException;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
import com.openisle.service.EmailSender; import com.openisle.service.EmailSender;
import java.time.Duration;
import java.time.ZoneId; import java.time.ZoneId;
import java.time.ZoneOffset; import java.time.ZoneOffset;
import java.util.*; import java.util.*;
@@ -79,13 +74,10 @@ public class PostService {
private final EmailSender emailSender; private final EmailSender emailSender;
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final PointService pointService; private final PointService pointService;
private final PostChangeLogService postChangeLogService;
private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>(); private final ConcurrentMap<Long, ScheduledFuture<?>> scheduledFinalizations = new ConcurrentHashMap<>();
@Value("${app.website-url:https://www.open-isle.com}") @Value("${app.website-url:https://www.open-isle.com}")
private String websiteUrl; private String websiteUrl;
private final RedisTemplate redisTemplate;
@org.springframework.beans.factory.annotation.Autowired @org.springframework.beans.factory.annotation.Autowired
public PostService(PostRepository postRepository, public PostService(PostRepository postRepository,
UserRepository userRepository, UserRepository userRepository,
@@ -107,9 +99,7 @@ public class PostService {
EmailSender emailSender, EmailSender emailSender,
ApplicationContext applicationContext, ApplicationContext applicationContext,
PointService pointService, PointService pointService,
PostChangeLogService postChangeLogService, @Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode) {
@Value("${app.post.publish-mode:DIRECT}") PublishMode publishMode,
RedisTemplate redisTemplate) {
this.postRepository = postRepository; this.postRepository = postRepository;
this.userRepository = userRepository; this.userRepository = userRepository;
this.categoryRepository = categoryRepository; this.categoryRepository = categoryRepository;
@@ -130,10 +120,7 @@ public class PostService {
this.emailSender = emailSender; this.emailSender = emailSender;
this.applicationContext = applicationContext; this.applicationContext = applicationContext;
this.pointService = pointService; this.pointService = pointService;
this.postChangeLogService = postChangeLogService;
this.publishMode = publishMode; this.publishMode = publishMode;
this.redisTemplate = redisTemplate;
} }
@EventListener(ApplicationReadyEvent.class) @EventListener(ApplicationReadyEvent.class)
@@ -172,28 +159,19 @@ public class PostService {
return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable); return postRepository.findByStatusAndRssExcludedFalseOrderByCreatedAtDesc(PostStatus.PUBLISHED, pageable);
} }
public Post excludeFromRss(Long id, String username) { public Post excludeFromRss(Long id) {
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
post.setRssExcluded(true); post.setRssExcluded(true);
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, false);
return saved;
} }
public Post includeInRss(Long id, String username) { public Post includeInRss(Long id) {
Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); Post post = postRepository.findById(id).orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
boolean oldFeatured = !Boolean.TRUE.equals(post.getRssExcluded());
post.setRssExcluded(false); post.setRssExcluded(false);
Post saved = postRepository.save(post); post = postRepository.save(post);
postChangeLogService.recordFeaturedChange(saved, user, oldFeatured, true); notificationService.createNotification(post.getAuthor(), NotificationType.POST_FEATURED, post, null, null, null, null, null);
notificationService.createNotification(saved.getAuthor(), NotificationType.POST_FEATURED, saved, null, null, null, null, null); pointService.awardForFeatured(post.getAuthor().getUsername(), post.getId());
pointService.awardForFeatured(saved.getAuthor().getUsername(), saved.getId()); return post;
return saved;
} }
public Post createPost(String username, public Post createPost(String username,
@@ -210,9 +188,9 @@ public class PostService {
LocalDateTime endTime, LocalDateTime endTime,
java.util.List<String> options, java.util.List<String> options,
Boolean multiple) { Boolean multiple) {
// 限制访问次数 long recent = postRepository.countByAuthorAfter(username,
boolean limitResult = postRateLimit(username); java.time.LocalDateTime.now().minusMinutes(5));
if (!limitResult) { if (recent >= 1) {
throw new RateLimitException("Too many posts"); throw new RateLimitException("Too many posts");
} }
if (tagIds == null || tagIds.isEmpty()) { if (tagIds == null || tagIds.isEmpty()) {
@@ -309,23 +287,6 @@ public class PostService {
return post; return post;
} }
/**
* 限制发帖频率
* @param username
* @return
*/
private boolean postRateLimit(String username){
String key = CachingConfig.LIMIT_CACHE_NAME +":posts:"+username;
String result = (String)redisTemplate.opsForValue().get(key);
//最近没有创建过文章
if(StringUtils.isEmpty(result)){
// 限制频率为5分钟
redisTemplate.opsForValue().set(key,"1", Duration.ofMinutes(5));
return true;
}
return false;
}
public void joinLottery(Long postId, String username) { public void joinLottery(Long postId, String username) {
LotteryPost post = lotteryPostRepository.findById(postId) LotteryPost post = lotteryPostRepository.findById(postId)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
@@ -394,7 +355,6 @@ public class PostService {
for (User participant : pp.getParticipants()) { for (User participant : pp.getParticipants()) {
notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, pp, null, null, null, null, null); notificationService.createNotification(participant, NotificationType.POLL_RESULT_PARTICIPANT, pp, null, null, null, null, null);
} }
postChangeLogService.recordVoteResult(pp);
}); });
} }
@@ -429,7 +389,6 @@ public class PostService {
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null); notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);
notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId())); notificationService.sendCustomPush(lp.getAuthor(), "抽奖已开奖", String.format("%s/posts/%d", websiteUrl, lp.getId()));
} }
postChangeLogService.recordLotteryResult(lp);
}); });
} }
@@ -679,28 +638,18 @@ public class PostService {
return post; return post;
} }
public Post pinPost(Long id, String username) { public Post pinPost(Long id) {
Post post = postRepository.findById(id) Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
java.time.LocalDateTime oldPinned = post.getPinnedAt();
post.setPinnedAt(java.time.LocalDateTime.now()); post.setPinnedAt(java.time.LocalDateTime.now());
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordPinnedChange(saved, user, oldPinned, saved.getPinnedAt());
return saved;
} }
public Post unpinPost(Long id, String username) { public Post unpinPost(Long id) {
Post post = postRepository.findById(id) Post post = postRepository.findById(id)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found")); .orElseThrow(() -> new com.openisle.exception.NotFoundException("Post not found"));
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
java.time.LocalDateTime oldPinned = post.getPinnedAt();
post.setPinnedAt(null); post.setPinnedAt(null);
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordPinnedChange(saved, user, oldPinned, null);
return saved;
} }
public Post closePost(Long id, String username) { public Post closePost(Long id, String username) {
@@ -711,11 +660,8 @@ public class PostService {
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) { if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
throw new IllegalArgumentException("Unauthorized"); throw new IllegalArgumentException("Unauthorized");
} }
boolean oldClosed = post.isClosed();
post.setClosed(true); post.setClosed(true);
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordClosedChange(saved, user, oldClosed, true);
return saved;
} }
public Post reopenPost(Long id, String username) { public Post reopenPost(Long id, String username) {
@@ -726,11 +672,8 @@ public class PostService {
if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) { if (!user.getId().equals(post.getAuthor().getId()) && user.getRole() != Role.ADMIN) {
throw new IllegalArgumentException("Unauthorized"); throw new IllegalArgumentException("Unauthorized");
} }
boolean oldClosed = post.isClosed();
post.setClosed(false); post.setClosed(false);
Post saved = postRepository.save(post); return postRepository.save(post);
postChangeLogService.recordClosedChange(saved, user, oldClosed, false);
return saved;
} }
@org.springframework.transaction.annotation.Transactional @org.springframework.transaction.annotation.Transactional
@@ -759,30 +702,14 @@ public class PostService {
if (tags.isEmpty()) { if (tags.isEmpty()) {
throw new IllegalArgumentException("Tag not found"); throw new IllegalArgumentException("Tag not found");
} }
String oldTitle = post.getTitle();
String oldContent = post.getContent();
Category oldCategory = post.getCategory();
java.util.Set<com.openisle.model.Tag> oldTags = new java.util.HashSet<>(post.getTags());
post.setTitle(title); post.setTitle(title);
String oldContent = post.getContent();
post.setContent(content); post.setContent(content);
post.setCategory(category); post.setCategory(category);
post.setTags(new java.util.HashSet<>(tags)); post.setTags(new java.util.HashSet<>(tags));
Post updated = postRepository.save(post); Post updated = postRepository.save(post);
imageUploader.adjustReferences(oldContent, content); imageUploader.adjustReferences(oldContent, content);
notificationService.notifyMentions(content, user, updated, null); notificationService.notifyMentions(content, user, updated, null);
if (!java.util.Objects.equals(oldTitle, title)) {
postChangeLogService.recordTitleChange(updated, user, oldTitle, title);
}
if (!java.util.Objects.equals(oldContent, content)) {
postChangeLogService.recordContentChange(updated, user, oldContent, content);
}
if (!java.util.Objects.equals(oldCategory.getId(), category.getId())) {
postChangeLogService.recordCategoryChange(updated, user, oldCategory.getName(), category.getName());
}
java.util.Set<com.openisle.model.Tag> newTags = new java.util.HashSet<>(tags);
if (!oldTags.equals(newTags)) {
postChangeLogService.recordTagChange(updated, user, oldTags, newTags);
}
return updated; return updated;
} }

View File

@@ -18,9 +18,6 @@ public class ResendEmailSender extends EmailSender {
@Value("${resend.api.key}") @Value("${resend.api.key}")
private String apiKey; private String apiKey;
@Value("${resend.from.email}")
private String fromEmail;
private final RestTemplate restTemplate = new RestTemplate(); private final RestTemplate restTemplate = new RestTemplate();
@Override @Override
@@ -36,7 +33,7 @@ public class ResendEmailSender extends EmailSender {
body.put("to", to); body.put("to", to);
body.put("subject", subject); body.put("subject", subject);
body.put("text", text); body.put("text", text);
body.put("from", "openisle <" + fromEmail + ">"); body.put("from", "openisle <noreply@chenjiating.com>"); // todo(tim): use config
HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers); HttpEntity<Map<String, String>> entity = new HttpEntity<>(body, headers);
restTemplate.exchange(url, HttpMethod.POST, entity, String.class); restTemplate.exchange(url, HttpMethod.POST, entity, String.class);

View File

@@ -1,6 +1,5 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.model.Role; import com.openisle.model.Role;
import com.openisle.service.PasswordValidator; import com.openisle.service.PasswordValidator;
@@ -8,18 +7,13 @@ import com.openisle.service.UsernameValidator;
import com.openisle.service.AvatarGenerator; import com.openisle.service.AvatarGenerator;
import com.openisle.exception.FieldException; import com.openisle.exception.FieldException;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.util.VerifyType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -31,10 +25,6 @@ public class UserService {
private final ImageUploader imageUploader; private final ImageUploader imageUploader;
private final AvatarGenerator avatarGenerator; private final AvatarGenerator avatarGenerator;
private final RedisTemplate redisTemplate;
private final EmailSender emailService;
public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) { public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) {
usernameValidator.validate(username); usernameValidator.validate(username);
passwordValidator.validate(password); passwordValidator.validate(password);
@@ -48,7 +38,7 @@ public class UserService {
// 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码 // 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码
u.setEmail(email); // 若不允许改邮箱可去掉 u.setEmail(email); // 若不允许改邮箱可去掉
u.setPassword(passwordEncoder.encode(password)); u.setPassword(passwordEncoder.encode(password));
// u.setVerificationCode(genCode()); u.setVerificationCode(genCode());
u.setRegisterReason(reason); u.setRegisterReason(reason);
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(u); return userRepository.save(u);
@@ -64,7 +54,7 @@ public class UserService {
// 未验证 → 允许“重注册” // 未验证 → 允许“重注册”
u.setUsername(username); // 若不允许改用户名可去掉 u.setUsername(username); // 若不允许改用户名可去掉
u.setPassword(passwordEncoder.encode(password)); u.setPassword(passwordEncoder.encode(password));
// u.setVerificationCode(genCode()); u.setVerificationCode(genCode());
u.setRegisterReason(reason); u.setRegisterReason(reason);
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(u); return userRepository.save(u);
@@ -77,7 +67,7 @@ public class UserService {
user.setPassword(passwordEncoder.encode(password)); user.setPassword(passwordEncoder.encode(password));
user.setRole(Role.USER); user.setRole(Role.USER);
user.setVerified(false); user.setVerified(false);
// user.setVerificationCode(genCode()); user.setVerificationCode(genCode());
user.setAvatar(avatarGenerator.generate(username)); user.setAvatar(avatarGenerator.generate(username));
user.setRegisterReason(reason); user.setRegisterReason(reason);
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
@@ -87,7 +77,7 @@ public class UserService {
public User registerWithInvite(String username, String email, String password) { public User registerWithInvite(String username, String email, String password) {
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT); User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
user.setVerified(true); user.setVerified(true);
// user.setVerificationCode(genCode()); user.setVerificationCode(genCode());
return userRepository.save(user); return userRepository.save(user);
} }
@@ -95,58 +85,16 @@ public class UserService {
return String.format("%06d", new Random().nextInt(1000000)); return String.format("%06d", new Random().nextInt(1000000));
} }
/** public boolean verifyCode(String username, String code) {
* 将验证码存入缓存,并发送邮件 Optional<User> userOpt = userRepository.findByUsername(username);
* @param user if (userOpt.isPresent() && code.equals(userOpt.get().getVerificationCode())) {
*/ User user = userOpt.get();
public void sendVerifyMail(User user, VerifyType verifyType){
//缓存验证码
String code = genCode();
String key;
String subject;
String content = "您的验证码是:" + code;
// 注册类型
if(verifyType.equals(VerifyType.REGISTER)){
key = CachingConfig.VERIFY_CACHE_NAME + ":register:code:" + user.getUsername();
subject = "在网站填写验证码以验证(有效期为5分钟)";
}else {
// 重置密码
key = CachingConfig.VERIFY_CACHE_NAME + ":reset_password:code:" + user.getUsername();
subject = "请填写验证码以重置密码(有效期为5分钟)";
}
redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);// 五分钟后验证码过期
emailService.sendEmail(user.getEmail(), subject, content);
}
/**
* 验证code是否正确
* @param user
* @param code
* @param verifyType
* @return
*/
public boolean verifyCode(User user, String code, VerifyType verifyType) {
// 生成key
String key1 = VerifyType.REGISTER.equals(verifyType)?":register:code:":":reset_password:code:";
String key = CachingConfig.VERIFY_CACHE_NAME + key1 + user.getUsername();
// 这里不能使用getAndDelete,需要6.x版本
String cachedCode = (String)redisTemplate.opsForValue().get(key);
// 如果校验code过期或者不存在
// 或者校验code不一致
if(Objects.isNull(cachedCode)
|| !cachedCode.equals(code)){
return false;
}
// 注册模式需要设置已经确认
if(VerifyType.REGISTER.equals(verifyType)){
user.setVerified(true); user.setVerified(true);
user.setVerificationCode(null);
userRepository.save(user); userRepository.save(user);
return true;
} }
// 走到这里说明验证成功删除验证码 return false;
redisTemplate.delete(key);
return true;
} }
public Optional<User> authenticate(String username, String password) { public Optional<User> authenticate(String username, String password) {
@@ -217,6 +165,26 @@ public class UserService {
return userRepository.save(user); return userRepository.save(user);
} }
public String generatePasswordResetCode(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
String code = genCode();
user.setPasswordResetCode(code);
userRepository.save(user);
return code;
}
public boolean verifyPasswordResetCode(String email, String code) {
Optional<User> userOpt = userRepository.findByEmail(email);
if (userOpt.isPresent() && code.equals(userOpt.get().getPasswordResetCode())) {
User user = userOpt.get();
user.setPasswordResetCode(null);
userRepository.save(user);
return true;
}
return false;
}
public User updatePassword(String username, String newPassword) { public User updatePassword(String username, String newPassword) {
passwordValidator.validate(newPassword); passwordValidator.validate(newPassword);
User user = userRepository.findByUsername(username) User user = userRepository.findByUsername(username)

View File

@@ -1,7 +1,6 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.exception.FieldException; import com.openisle.exception.FieldException;
import org.apache.commons.lang3.math.NumberUtils;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
/** /**
@@ -18,11 +17,6 @@ public class UsernameValidator {
if (username == null || username.isEmpty()) { if (username == null || username.isEmpty()) {
throw new FieldException("username", "Username cannot be empty"); throw new FieldException("username", "Username cannot be empty");
} }
if (NumberUtils.isDigits(username)) {
throw new FieldException("username", "Username cannot be pure number");
}
} }
} }

View File

@@ -1,20 +0,0 @@
package com.openisle.util;
/**
* 验证码类型
* @author smallclover
* @since 2025-09-08
*/
public enum VerifyType {
REGISTER(1),
RESET_PASSWORD(2);
private final int code;
VerifyType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}

View File

@@ -12,7 +12,7 @@ spring.jpa.hibernate.ddl-auto=update
# for redis # for redis
spring.data.redis.host=${REDIS_HOST:localhost} spring.data.redis.host=${REDIS_HOST:localhost}
spring.data.redis.port=${REDIS_PORT:6379} spring.data.redis.port=${REDIS_PORT:6379}
spring.data.redis.database=${REDIS_DATABASE:0} spring.data.redis.database=0
# for jwt # for jwt
app.jwt.secret=${JWT_SECRET:jwt_sec} app.jwt.secret=${JWT_SECRET:jwt_sec}
@@ -56,10 +56,8 @@ app.captcha.comment-enabled=${CAPTCHA_COMMENT_ENABLED:false}
# ========= Optional ========= # ========= Optional =========
# for resend email send service, you can improve your service by yourself # for resend email send service, you can improve your service by yourself
resend.api.key=${RESEND_API_KEY:} resend.api.key=${RESEND_API_KEY:}
resend.from.email=${RESEND_FROM_EMAIL:}
# your email services: ... # your email services: ...
# for tencent cloud image upload service, you can improve your service by yourself # for tencent cloud image upload service, you can improve your service by yourself
cos.base-url=${:https://example.com} cos.base-url=${:https://example.com}
cos.secret-id=${COS_SECRET_ID:} cos.secret-id=${COS_SECRET_ID:}
@@ -108,10 +106,6 @@ rabbitmq.sharding.enabled=true
# see https://springdoc.org/#springdoc-openapi-core-properties # see https://springdoc.org/#springdoc-openapi-core-properties
springdoc.api-docs.path=/api/v3/api-docs springdoc.api-docs.path=/api/v3/api-docs
springdoc.api-docs.enabled=true springdoc.api-docs.enabled=true
springdoc.api-docs.servers[0].url=https://www.open-isle.com
springdoc.api-docs.servers[0].description=Production Environment
springdoc.api-docs.servers[1].url=https://www.staging.open-isle.com
springdoc.api-docs.servers[1].description=Staging Environment
springdoc.info.title=OpenIsle springdoc.info.title=OpenIsle
springdoc.info.description=OpenIsle Open API Documentation springdoc.info.description=OpenIsle Open API Documentation
springdoc.info.version=0.0.1 springdoc.info.version=0.0.1

View File

@@ -35,9 +35,9 @@ DELETE FROM `users`;
-- 插入用户,两个普通用户,一个管理员 -- 插入用户,两个普通用户,一个管理员
-- username:admin/user1/user2 password:123321 -- username:admin/user1/user2 password:123321
INSERT INTO `users` (`id`, `approved`, `avatar`, `created_at`, `display_medal`, `email`, `experience`, `introduction`, `password`, `password_reset_code`, `point`, `register_reason`, `role`, `username`, `verification_code`, `verified`) VALUES INSERT INTO `users` (`id`, `approved`, `avatar`, `created_at`, `display_medal`, `email`, `experience`, `introduction`, `password`, `password_reset_code`, `point`, `register_reason`, `role`, `username`, `verification_code`, `verified`) VALUES
(1, b'1', '', '2025-09-01 16:08:17.426430', 'PIONEER', 'adminmail@openisle.com', 70, NULL, '$2a$10$dux.NXwW09cCsdZ05BgcnOtxVqqjcmnbj3.8xcxGl/iiIlv06y7Oe', NULL, 110, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'ADMIN', 'admin', NULL, b'1'), (1, b'1', '', '2025-09-01 16:08:17.426430', 'PIONEER', 'adminmail@openisle.com', 70, NULL, '$2a$10$m.lLbT3wFtnzFMi7JqN17ecv/dzH704WzU1f/xvQ0nVz4XxTXPT0K', NULL, 110, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'ADMIN', 'admin', NULL, b'1'),
(2, b'1', '', '2025-09-03 16:08:17.426430', 'PIONEER', 'usermail2@openisle.com', 70, NULL, '$2a$10$dux.NXwW09cCsdZ05BgcnOtxVqqjcmnbj3.8xcxGl/iiIlv06y7Oe', NULL, 110, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'USER', 'user1', NULL, b'1'), (2, b'1', '', '2025-09-03 16:08:17.426430', 'PIONEER', 'usermail2@openisle.com', 70, NULL, '$2a$10$m.lLbT3wFtnzFMi7JqN17ecv/dzH704WzU1f/xvQ0nVz4XxTXPT0K', NULL, 110, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'USER', 'user1', NULL, b'1'),
(3, b'1', '', '2025-09-02 17:21:21.617666', 'PIONEER', 'usermail1@openisle.com', 40, NULL, '$2a$10$dux.NXwW09cCsdZ05BgcnOtxVqqjcmnbj3.8xcxGl/iiIlv06y7Oe', NULL, 40, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'USER', 'user2', NULL, b'1'); (3, b'1', '', '2025-09-02 17:21:21.617666', 'PIONEER', 'usermail1@openisle.com', 40, NULL, '$2a$10$m.lLbT3wFtnzFMi7JqN17ecv/dzH704WzU1f/xvQ0nVz4XxTXPT0K', NULL, 40, '测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试测试', 'USER', 'user2', NULL, b'1');
-- 创建 tags 表(如果不存在) -- 创建 tags 表(如果不存在)
CREATE TABLE IF NOT EXISTS `tags` ( CREATE TABLE IF NOT EXISTS `tags` (

View File

@@ -4,7 +4,6 @@ import com.openisle.model.User;
import com.openisle.service.*; import com.openisle.service.*;
import com.openisle.model.RegisterMode; import com.openisle.model.RegisterMode;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.util.VerifyType;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -72,9 +71,7 @@ class AuthControllerTest {
@Test @Test
void verifyCodeEndpoint() throws Exception { void verifyCodeEndpoint() throws Exception {
User user = new User(); Mockito.when(userService.verifyCode("u", "123")).thenReturn(true);
user.setUsername("u");
Mockito.when(userService.verifyCode(user, "123", VerifyType.REGISTER)).thenReturn(true);
Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token"); Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token");
mockMvc.perform(post("/api/auth/verify") mockMvc.perform(post("/api/auth/verify")

View File

@@ -1,67 +0,0 @@
package com.openisle.service;
import com.openisle.model.PointHistory;
import com.openisle.model.PointHistoryType;
import com.openisle.model.Role;
import com.openisle.model.User;
import com.openisle.repository.PointHistoryRepository;
import com.openisle.repository.UserRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.annotation.Import;
import java.time.LocalDateTime;
import static org.junit.jupiter.api.Assertions.assertEquals;
@DataJpaTest
@Import(PointService.class)
class PointServiceRecalculateUserPointsTest {
@Autowired
private PointService pointService;
@Autowired
private UserRepository userRepository;
@Autowired
private PointHistoryRepository pointHistoryRepository;
@Test
void recalculatesBalanceAfterDeletion() {
User user = new User();
user.setUsername("u");
user.setEmail("u@example.com");
user.setPassword("p");
user.setRole(Role.USER);
userRepository.save(user);
PointHistory h1 = new PointHistory();
h1.setUser(user);
h1.setType(PointHistoryType.POST);
h1.setAmount(30);
h1.setBalance(30);
h1.setCreatedAt(LocalDateTime.now().minusMinutes(2));
pointHistoryRepository.save(h1);
PointHistory h2 = new PointHistory();
h2.setUser(user);
h2.setType(PointHistoryType.COMMENT);
h2.setAmount(10);
h2.setBalance(40);
h2.setCreatedAt(LocalDateTime.now().minusMinutes(1));
pointHistoryRepository.save(h2);
user.setPoint(40);
userRepository.save(user);
pointHistoryRepository.delete(h1);
int total = pointService.recalculateUserPoints(user);
assertEquals(10, total);
assertEquals(10, userRepository.findById(user.getId()).orElseThrow().getPoint());
assertEquals(10, pointHistoryRepository.findById(h2.getId()).orElseThrow().getBalance());
}
}

View File

@@ -6,7 +6,6 @@ import com.openisle.exception.RateLimitException;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.scheduling.TaskScheduler; import org.springframework.scheduling.TaskScheduler;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@@ -38,13 +37,11 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate); imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -89,13 +86,11 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate); imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -146,13 +141,11 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate); imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L); when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
@@ -184,13 +177,11 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
RedisTemplate redisTemplate = mock(RedisTemplate.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT, redisTemplate); imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
User author = new User(); User author = new User();

9
docs/.gitignore vendored
View File

@@ -1,9 +0,0 @@
# generated content
.contentlayer
.content-collections
.source
.next/
.vercel
next-env.d.ts
openapi.json
openapi.yaml

View File

@@ -1,21 +0,0 @@
# OpenIsle Documentation
## 要求
使用 [bun](https://bun.com/) 作为工具链的运行时,版本 1.2+。
使用 [Fumadocs](https://fumadocs.dev/) 作为文档工具。
## 开发调试
```bash
bun install
bun dev
```
## 项目结构
使用以下路由:
- `frontend/` 前端技术文档
- `backend/` 后端技术文档
- `openapi/` 后端 API 文档

View File

@@ -1,61 +0,0 @@
import { source } from '@/lib/source';
import { DocsBody, DocsDescription, DocsPage, DocsTitle } from 'fumadocs-ui/page';
import type { Metadata } from 'next';
import { notFound } from 'next/navigation';
import { createRelativeLink } from 'fumadocs-ui/mdx';
import { getMDXComponents } from '@/mdx-components';
import { Card, Cards } from 'fumadocs-ui/components/card';
import { getPageTreePeers } from 'fumadocs-core/server';
function DocsCategory({ url }: { url: string }) {
return (
<Cards>
{getPageTreePeers(source.pageTree, url).map((peer) => (
<Card key={peer.url} title={peer.name} href={peer.url}>
<span className="font-mono">{peer.url}</span>
</Card>
))}
</Cards>
);
}
export default async function Page(props: PageProps<'/[[...slug]]'>) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
const MDXContent = page.data.body;
return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription>{page.data.description}</DocsDescription>
<DocsBody>
<MDXContent
components={getMDXComponents({
// this allows you to link to other pages with relative file paths
a: createRelativeLink(source, page),
})}
/>
{page.data.full ? null : <DocsCategory url={page.url} />}
</DocsBody>
</DocsPage>
);
}
export async function generateStaticParams() {
return source.generateParams();
}
export async function generateMetadata(
props: PageProps<'/[[...slug]]'>
): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();
return {
title: page.data.title,
description: page.data.description,
};
}

View File

@@ -1,77 +0,0 @@
import { DocsLayout } from 'fumadocs-ui/layouts/docs';
import { baseOptions } from '@/lib/layout.shared';
import { source } from '@/lib/source';
import { CodeXmlIcon, CompassIcon, ServerIcon } from 'lucide-react';
function TabIcon({
color = 'var(--color-fd-foreground)',
children,
}: {
color?: string;
children: React.ReactNode;
}) {
return (
<div
className="[&_svg]:size-full rounded-lg size-full text-(--tab-color) max-md:bg-(--tab-color)/10 max-md:border max-md:p-1.5"
style={
{
'--tab-color': color,
} as React.CSSProperties
}
>
{children}
</div>
);
}
function TabTitle({ children }: { children: React.ReactNode }) {
return <span className="text-[11px]">{children}</span>;
}
export default function Layout({ children }: LayoutProps<'/'>) {
return (
// @ts-ignore
<DocsLayout
tree={source.pageTree}
sidebar={{
defaultOpenLevel: 1,
prefetch: true,
tabs: [
{
title: 'OpenIsle 前端',
description: <TabTitle></TabTitle>,
url: '/frontend',
icon: (
<TabIcon color="#4ca154">
<CompassIcon />
</TabIcon>
),
},
{
title: 'OpenIsle 后端',
description: <TabTitle></TabTitle>,
url: '/backend',
icon: (
<TabIcon color="#1f66f4">
<ServerIcon />
</TabIcon>
),
},
{
title: 'OpenIsle API',
description: <TabTitle> API </TabTitle>,
url: '/openapi',
icon: (
<TabIcon color="#677489">
<CodeXmlIcon />
</TabIcon>
),
},
],
}}
{...baseOptions()}
>
{children}
</DocsLayout>
);
}

View File

@@ -1,4 +0,0 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
@import 'fumadocs-openapi/css/preset.css';

View File

@@ -1,17 +0,0 @@
import '@/app/global.css';
import { Provider } from '@/app/provider';
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
});
export default function Layout({ children }: LayoutProps<'/'>) {
return (
<html lang="zh" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen">
<Provider>{children}</Provider>
</body>
</html>
);
}

View File

@@ -1,8 +0,0 @@
'use client';
import { RootProvider } from 'fumadocs-ui/provider';
import type { ReactNode } from 'react';
export function Provider({ children }: { children: ReactNode }) {
return <RootProvider>{children}</RootProvider>;
}

View File

@@ -1,844 +0,0 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "openisle-docs",
"dependencies": {
"fumadocs-core": "15.7.7",
"fumadocs-mdx": "11.8.2",
"fumadocs-openapi": "^9.3.4",
"fumadocs-ui": "15.7.7",
"lucide-react": "^0.542.0",
"next": "15.5.2",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"shiki": "^3.12.1",
},
"devDependencies": {
"@tailwindcss/postcss": "^4.1.12",
"@types/mdx": "^2.0.13",
"@types/node": "24.3.0",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.12",
"typescript": "^5.9.2",
},
},
},
"packages": {
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="],
"@apidevtools/json-schema-ref-parser": ["@apidevtools/json-schema-ref-parser@11.9.3", "", { "dependencies": { "@jsdevtools/ono": "^7.1.3", "@types/json-schema": "^7.0.15", "js-yaml": "^4.1.0" } }, "sha512-60vepv88RwcJtSHrD6MjIL6Ta3SOYbgfnkHb+ppAVK+o9mXprRtulx7VlRl3lN3bbvysAfCS7WMVfhUYemB0IQ=="],
"@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.9", "", { "os": "aix", "cpu": "ppc64" }, "sha512-OaGtL73Jck6pBKjNIe24BnFE6agGl+6KxDtTfHhy1HmhthfKouEcOhqpSL64K4/0WCtbKFLOdzD/44cJ4k9opA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.9", "", { "os": "android", "cpu": "arm" }, "sha512-5WNI1DaMtxQ7t7B6xa572XMXpHAaI/9Hnhk8lcxF4zVN4xstUgTlvuGDorBguKEnZO70qwEcLpfifMLoxiPqHQ=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.9", "", { "os": "android", "cpu": "arm64" }, "sha512-IDrddSmpSv51ftWslJMvl3Q2ZT98fUSL2/rlUXuVqRXHCs5EUF1/f+jbjF5+NG9UffUDMCiTyh8iec7u8RlTLg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.9", "", { "os": "android", "cpu": "x64" }, "sha512-I853iMZ1hWZdNllhVZKm34f4wErd4lMyeV7BLzEExGEIZYsOzqDWDf+y082izYUE8gtJnYHdeDpN/6tUdwvfiw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.9", "", { "os": "darwin", "cpu": "arm64" }, "sha512-XIpIDMAjOELi/9PB30vEbVMs3GV1v2zkkPnuyRRURbhqjyzIINwj+nbQATh4H9GxUgH1kFsEyQMxwiLFKUS6Rg=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.9", "", { "os": "darwin", "cpu": "x64" }, "sha512-jhHfBzjYTA1IQu8VyrjCX4ApJDnH+ez+IYVEoJHeqJm9VhG9Dh2BYaJritkYK3vMaXrf7Ogr/0MQ8/MeIefsPQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.9", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-z93DmbnY6fX9+KdD4Ue/H6sYs+bhFQJNCPZsi4XWJoYblUqT06MQUdBCpcSfuiN72AbqeBFu5LVQTjfXDE2A6Q=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.9", "", { "os": "freebsd", "cpu": "x64" }, "sha512-mrKX6H/vOyo5v71YfXWJxLVxgy1kyt1MQaD8wZJgJfG4gq4DpQGpgTB74e5yBeQdyMTbgxp0YtNj7NuHN0PoZg=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.9", "", { "os": "linux", "cpu": "arm" }, "sha512-HBU2Xv78SMgaydBmdor38lg8YDnFKSARg1Q6AT0/y2ezUAKiZvc211RDFHlEZRFNRVhcMamiToo7bDx3VEOYQw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.9", "", { "os": "linux", "cpu": "arm64" }, "sha512-BlB7bIcLT3G26urh5Dmse7fiLmLXnRlopw4s8DalgZ8ef79Jj4aUcYbk90g8iCa2467HX8SAIidbL7gsqXHdRw=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.9", "", { "os": "linux", "cpu": "ia32" }, "sha512-e7S3MOJPZGp2QW6AK6+Ly81rC7oOSerQ+P8L0ta4FhVi+/j/v2yZzx5CqqDaWjtPFfYz21Vi1S0auHrap3Ma3A=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-Sbe10Bnn0oUAB2AalYztvGcK+o6YFFA/9829PhOCUS9vkJElXGdphz0A3DbMdP8gmKkqPmPcMJmJOrI3VYB1JQ=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-YcM5br0mVyZw2jcQeLIkhWtKPeVfAerES5PvOzaDxVtIyZ2NUBZKNLjC5z3/fUlDgT6w89VsxP2qzNipOaaDyA=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.9", "", { "os": "linux", "cpu": "ppc64" }, "sha512-++0HQvasdo20JytyDpFvQtNrEsAgNG2CY1CLMwGXfFTKGBGQT3bOeLSYE2l1fYdvML5KUuwn9Z8L1EWe2tzs1w=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.9", "", { "os": "linux", "cpu": "none" }, "sha512-uNIBa279Y3fkjV+2cUjx36xkx7eSjb8IvnL01eXUKXez/CBHNRw5ekCGMPM0BcmqBxBcdgUWuUXmVWwm4CH9kg=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.9", "", { "os": "linux", "cpu": "s390x" }, "sha512-Mfiphvp3MjC/lctb+7D287Xw1DGzqJPb/J2aHHcHxflUo+8tmN/6d4k6I2yFR7BVo5/g7x2Monq4+Yew0EHRIA=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.9", "", { "os": "linux", "cpu": "x64" }, "sha512-iSwByxzRe48YVkmpbgoxVzn76BXjlYFXC7NvLYq+b+kDjyyk30J0JY47DIn8z1MO3K0oSl9fZoRmZPQI4Hklzg=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-9jNJl6FqaUG+COdQMjSCGW4QiMHH88xWbvZ+kRVblZsWrkXlABuGdFJ1E9L7HK+T0Yqd4akKNa/lO0+jDxQD4Q=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.9", "", { "os": "none", "cpu": "x64" }, "sha512-RLLdkflmqRG8KanPGOU7Rpg829ZHu8nFy5Pqdi9U01VYtG9Y0zOG6Vr2z4/S+/3zIyOxiK6cCeYNWOFR9QP87g=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.9", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-YaFBlPGeDasft5IIM+CQAhJAqS3St3nJzDEgsgFixcfZeyGPCd6eJBWzke5piZuZ7CtL656eOSYKk4Ls2C0FRQ=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.9", "", { "os": "openbsd", "cpu": "x64" }, "sha512-1MkgTCuvMGWuqVtAvkpkXFmtL8XhWy+j4jaSO2wxfJtilVCi0ZE37b8uOdMItIHz4I6z1bWWtEX4CJwcKYLcuA=="],
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.25.9", "", { "os": "none", "cpu": "arm64" }, "sha512-4Xd0xNiMVXKh6Fa7HEJQbrpP3m3DDn43jKxMjxLLRjWnRsfxjORYJlXPO4JNcXtOyfajXorRKY9NkOpTHptErg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.9", "", { "os": "sunos", "cpu": "x64" }, "sha512-WjH4s6hzo00nNezhp3wFIAfmGZ8U7KtrJNlFMRKxiI9mxEK1scOMAaa9i4crUtu+tBr+0IN6JCuAcSBJZfnphw=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.9", "", { "os": "win32", "cpu": "arm64" }, "sha512-mGFrVJHmZiRqmP8xFOc6b84/7xa5y5YvR1x8djzXpJBSv/UsNK6aqec+6JDjConTgvvQefdGhFDAs2DLAds6gQ=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.9", "", { "os": "win32", "cpu": "ia32" }, "sha512-b33gLVU2k11nVx1OhX3C8QQP6UHQK4ZtN56oFWvVXvz2VkDoe6fbG8TOgHFxEvqeqohmRnIHe5A1+HADk4OQww=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.9", "", { "os": "win32", "cpu": "x64" }, "sha512-PPOl1mi6lpLNQxnGoyAfschAodRFYXJ+9fs6WHXz7CSWKbOqiMZsubC+BQsVKuul+3vKLuwTHsS2c2y9EoKwxQ=="],
"@floating-ui/core": ["@floating-ui/core@1.7.3", "", { "dependencies": { "@floating-ui/utils": "^0.2.10" } }, "sha512-sGnvb5dmrJaKEZ+LDIpguvdX3bDlEllmv4/ClQ9awcmCZrlx5jQyyMWFM5kBI+EyNOCDDiKk8il0zeuX3Zlg/w=="],
"@floating-ui/dom": ["@floating-ui/dom@1.7.4", "", { "dependencies": { "@floating-ui/core": "^1.7.3", "@floating-ui/utils": "^0.2.10" } }, "sha512-OOchDgh4F2CchOX94cRVqhvy7b3AFb+/rQXyswmzmGakRfkMgoWVjfnLWkRirfLEfuD4ysVW16eXzwt3jHIzKA=="],
"@floating-ui/react-dom": ["@floating-ui/react-dom@2.1.6", "", { "dependencies": { "@floating-ui/dom": "^1.7.4" }, "peerDependencies": { "react": ">=16.8.0", "react-dom": ">=16.8.0" } }, "sha512-4JX6rEatQEvlmgU80wZyq9RT96HZJa88q8hp0pBd+LrczeDI4o6uA2M+uvxngVHo4Ihr8uibXxH6+70zhAFrVw=="],
"@floating-ui/utils": ["@floating-ui/utils@0.2.10", "", {}, "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ=="],
"@formatjs/intl-localematcher": ["@formatjs/intl-localematcher@0.6.1", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-ePEgLgVCqi2BBFnTMWPfIghu6FkbZnnBVhO2sSxvLfrdFw7wCHAHiDoM2h4NRgjbaY7+B7HgOLZGkK187pZTZg=="],
"@fumari/json-schema-to-typescript": ["@fumari/json-schema-to-typescript@1.1.3", "", { "dependencies": { "@apidevtools/json-schema-ref-parser": "^11.9.3", "js-yaml": "^4.1.0", "prettier": "^3.5.3" } }, "sha512-KnaZAo5W769nOaxhPqEMTdjHdngugxmPpNS+Yr2U90iVxgmNAWwhSr8Nx3l+CUehJKNFzJi2C7clQXOfuPJegA=="],
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.0" }, "os": "darwin", "cpu": "arm64" }, "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg=="],
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.0" }, "os": "darwin", "cpu": "x64" }, "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA=="],
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ=="],
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg=="],
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.0", "", { "os": "linux", "cpu": "arm" }, "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw=="],
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA=="],
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ=="],
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw=="],
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg=="],
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q=="],
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.0", "", { "os": "linux", "cpu": "x64" }, "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q=="],
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.0" }, "os": "linux", "cpu": "arm" }, "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A=="],
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA=="],
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.0" }, "os": "linux", "cpu": "ppc64" }, "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA=="],
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.0" }, "os": "linux", "cpu": "s390x" }, "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ=="],
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ=="],
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.0" }, "os": "linux", "cpu": "arm64" }, "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ=="],
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.3", "", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.0" }, "os": "linux", "cpu": "x64" }, "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ=="],
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.3", "", { "dependencies": { "@emnapi/runtime": "^1.4.4" }, "cpu": "none" }, "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg=="],
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ=="],
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.3", "", { "os": "win32", "cpu": "ia32" }, "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw=="],
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.3", "", { "os": "win32", "cpu": "x64" }, "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g=="],
"@isaacs/fs-minipass": ["@isaacs/fs-minipass@4.0.1", "", { "dependencies": { "minipass": "^7.0.4" } }, "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.30", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q=="],
"@jsdevtools/ono": ["@jsdevtools/ono@7.1.3", "", {}, "sha512-4JQNk+3mVzK3xh2rqd6RB4J46qUR19azEHBneZyTZM+c456qOrbbM/5xcR8huNCCcbVt7+UmizG6GuUvPvKUYg=="],
"@mdx-js/mdx": ["@mdx-js/mdx@3.1.1", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdx": "^2.0.0", "acorn": "^8.0.0", "collapse-white-space": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-util-scope": "^1.0.0", "estree-walker": "^3.0.0", "hast-util-to-jsx-runtime": "^2.0.0", "markdown-extensions": "^2.0.0", "recma-build-jsx": "^1.0.0", "recma-jsx": "^1.0.0", "recma-stringify": "^1.0.0", "rehype-recma": "^1.0.0", "remark-mdx": "^3.0.0", "remark-parse": "^11.0.0", "remark-rehype": "^11.0.0", "source-map": "^0.7.0", "unified": "^11.0.0", "unist-util-position-from-estree": "^2.0.0", "unist-util-stringify-position": "^4.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ=="],
"@next/env": ["@next/env@15.5.2", "", {}, "sha512-Qe06ew4zt12LeO6N7j8/nULSOe3fMXE4dM6xgpBQNvdzyK1sv5y4oAP3bq4LamrvGCZtmRYnW8URFCeX5nFgGg=="],
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@15.5.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-8bGt577BXGSd4iqFygmzIfTYizHb0LGWqH+qgIF/2EDxS5JsSdERJKA8WgwDyNBZgTIIA4D8qUtoQHmxIIquoQ=="],
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@15.5.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-2DjnmR6JHK4X+dgTXt5/sOCu/7yPtqpYt8s8hLkHFK3MGkka2snTv3yRMdHvuRtJVkPwCGsvBSwmoQCHatauFQ=="],
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@15.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3j7SWDBS2Wov/L9q0mFJtEvQ5miIqfO4l7d2m9Mo06ddsgUK8gWfHGgbjdFlCp2Ek7MmMQZSxpGFqcC8zGh2AA=="],
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@15.5.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-s6N8k8dF9YGc5T01UPQ08yxsK6fUow5gG1/axWc1HVVBYQBgOjca4oUZF7s4p+kwhkB1bDSGR8QznWrFZ/Rt5g=="],
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@15.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-o1RV/KOODQh6dM6ZRJGZbc+MOAHww33Vbs5JC9Mp1gDk8cpEO+cYC/l7rweiEalkSm5/1WGa4zY7xrNwObN4+Q=="],
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@15.5.2", "", { "os": "linux", "cpu": "x64" }, "sha512-/VUnh7w8RElYZ0IV83nUcP/J4KJ6LLYliiBIri3p3aW2giF+PAVgZb6mk8jbQSB3WlTai8gEmCAr7kptFa1H6g=="],
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@15.5.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-sMPyTvRcNKXseNQ/7qRfVRLa0VhR0esmQ29DD6pqvG71+JdVnESJaHPA8t7bc67KD5spP3+DOCNLhqlEI2ZgQg=="],
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@15.5.2", "", { "os": "win32", "cpu": "x64" }, "sha512-W5VvyZHnxG/2ukhZF/9Ikdra5fdNftxI6ybeVKYvBPDtyx7x4jPPSNduUkfH5fo3zG0JQ0bPxgy41af2JX5D4Q=="],
"@orama/orama": ["@orama/orama@3.1.13", "", {}, "sha512-O0hdKt4K31i8fpq8Bw5RfdPVAqm0EdduBUcluPo2MRcfCOwUEf5JlnvRhf/J0ezOYOD8jQ/LumYZxOVi/XK/BA=="],
"@radix-ui/number": ["@radix-ui/number@1.1.1", "", {}, "sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g=="],
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
"@radix-ui/react-accordion": ["@radix-ui/react-accordion@1.2.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collapsible": "1.1.12", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA=="],
"@radix-ui/react-arrow": ["@radix-ui/react-arrow@1.1.7", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w=="],
"@radix-ui/react-collapsible": ["@radix-ui/react-collapsible@1.1.12", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA=="],
"@radix-ui/react-collection": ["@radix-ui/react-collection@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw=="],
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
"@radix-ui/react-context": ["@radix-ui/react-context@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA=="],
"@radix-ui/react-dialog": ["@radix-ui/react-dialog@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw=="],
"@radix-ui/react-direction": ["@radix-ui/react-direction@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw=="],
"@radix-ui/react-dismissable-layer": ["@radix-ui/react-dismissable-layer@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-escape-keydown": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg=="],
"@radix-ui/react-focus-guards": ["@radix-ui/react-focus-guards@1.1.3", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw=="],
"@radix-ui/react-focus-scope": ["@radix-ui/react-focus-scope@1.1.7", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw=="],
"@radix-ui/react-id": ["@radix-ui/react-id@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg=="],
"@radix-ui/react-navigation-menu": ["@radix-ui/react-navigation-menu@1.2.14", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w=="],
"@radix-ui/react-popover": ["@radix-ui/react-popover@1.1.15", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-controllable-state": "1.2.2", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA=="],
"@radix-ui/react-popper": ["@radix-ui/react-popper@1.2.8", "", { "dependencies": { "@floating-ui/react-dom": "^2.0.0", "@radix-ui/react-arrow": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-rect": "1.1.1", "@radix-ui/react-use-size": "1.1.1", "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw=="],
"@radix-ui/react-portal": ["@radix-ui/react-portal@1.1.9", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ=="],
"@radix-ui/react-presence": ["@radix-ui/react-presence@1.1.5", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ=="],
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
"@radix-ui/react-roving-focus": ["@radix-ui/react-roving-focus@1.1.11", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA=="],
"@radix-ui/react-scroll-area": ["@radix-ui/react-scroll-area@1.2.10", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A=="],
"@radix-ui/react-select": ["@radix-ui/react-select@2.2.6", "", { "dependencies": { "@radix-ui/number": "1.1.1", "@radix-ui/primitive": "1.1.3", "@radix-ui/react-collection": "1.1.7", "@radix-ui/react-compose-refs": "1.1.2", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-dismissable-layer": "1.1.11", "@radix-ui/react-focus-guards": "1.1.3", "@radix-ui/react-focus-scope": "1.1.7", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-popper": "1.2.8", "@radix-ui/react-portal": "1.1.9", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-slot": "1.2.3", "@radix-ui/react-use-callback-ref": "1.1.1", "@radix-ui/react-use-controllable-state": "1.2.2", "@radix-ui/react-use-layout-effect": "1.1.1", "@radix-ui/react-use-previous": "1.1.1", "@radix-ui/react-visually-hidden": "1.2.3", "aria-hidden": "^1.2.4", "react-remove-scroll": "^2.6.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ=="],
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
"@radix-ui/react-tabs": ["@radix-ui/react-tabs@1.1.13", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-context": "1.1.2", "@radix-ui/react-direction": "1.1.1", "@radix-ui/react-id": "1.1.1", "@radix-ui/react-presence": "1.1.5", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-roving-focus": "1.1.11", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A=="],
"@radix-ui/react-use-callback-ref": ["@radix-ui/react-use-callback-ref@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg=="],
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
"@radix-ui/react-use-escape-keydown": ["@radix-ui/react-use-escape-keydown@1.1.1", "", { "dependencies": { "@radix-ui/react-use-callback-ref": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g=="],
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
"@radix-ui/react-use-previous": ["@radix-ui/react-use-previous@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ=="],
"@radix-ui/react-use-rect": ["@radix-ui/react-use-rect@1.1.1", "", { "dependencies": { "@radix-ui/rect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w=="],
"@radix-ui/react-use-size": ["@radix-ui/react-use-size@1.1.1", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ=="],
"@radix-ui/react-visually-hidden": ["@radix-ui/react-visually-hidden@1.2.3", "", { "dependencies": { "@radix-ui/react-primitive": "2.1.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug=="],
"@radix-ui/rect": ["@radix-ui/rect@1.1.1", "", {}, "sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw=="],
"@scalar/openapi-parser": ["@scalar/openapi-parser@0.18.3", "", { "dependencies": { "@scalar/openapi-types": "0.3.7", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "2.8.0" } }, "sha512-j0OM3Y3JTVx50m7r5ogoADe2pT/ewhAnO7knjkcm7b+vjgKRpgjUMhXsYraBemFkqd2dNa1QfTX/mDVv7wEr9A=="],
"@scalar/openapi-types": ["@scalar/openapi-types@0.3.7", "", { "dependencies": { "zod": "3.24.1" } }, "sha512-QHSvHBVDze3+dUwAhIGq6l1iOev4jdoqdBK7QpfeN1Q4h+6qpVEw3EEqBiH0AXUSh/iWwObBv4uMgfIx0aNZ5g=="],
"@shikijs/core": ["@shikijs/core@3.12.2", "", { "dependencies": { "@shikijs/types": "3.12.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4", "hast-util-to-html": "^9.0.5" } }, "sha512-L1Safnhra3tX/oJK5kYHaWmLEBJi1irASwewzY3taX5ibyXyMkkSDZlq01qigjryOBwrXSdFgTiZ3ryzSNeu7Q=="],
"@shikijs/engine-javascript": ["@shikijs/engine-javascript@3.12.2", "", { "dependencies": { "@shikijs/types": "3.12.2", "@shikijs/vscode-textmate": "^10.0.2", "oniguruma-to-es": "^4.3.3" } }, "sha512-Nm3/azSsaVS7hk6EwtHEnTythjQfwvrO5tKqMlaH9TwG1P+PNaR8M0EAKZ+GaH2DFwvcr4iSfTveyxMIvXEHMw=="],
"@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.12.2", "", { "dependencies": { "@shikijs/types": "3.12.2", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-hozwnFHsLvujK4/CPVHNo3Bcg2EsnG8krI/ZQ2FlBlCRpPZW4XAEQmEwqegJsypsTAN9ehu2tEYe30lYKSZW/w=="],
"@shikijs/langs": ["@shikijs/langs@3.12.2", "", { "dependencies": { "@shikijs/types": "3.12.2" } }, "sha512-bVx5PfuZHDSHoBal+KzJZGheFuyH4qwwcwG/n+MsWno5cTlKmaNtTsGzJpHYQ8YPbB5BdEdKU1rga5/6JGY8ww=="],
"@shikijs/rehype": ["@shikijs/rehype@3.12.2", "", { "dependencies": { "@shikijs/types": "3.12.2", "@types/hast": "^3.0.4", "hast-util-to-string": "^3.0.1", "shiki": "3.12.2", "unified": "^11.0.5", "unist-util-visit": "^5.0.0" } }, "sha512-9wg+FKv0ByaQScTonpZdrDhADOoJP/yCWLAuiYYG6GehwNV5rGwnLvWKj33UmtLedKMSHzWUdB+Un6rfDFo/FA=="],
"@shikijs/themes": ["@shikijs/themes@3.12.2", "", { "dependencies": { "@shikijs/types": "3.12.2" } }, "sha512-fTR3QAgnwYpfGczpIbzPjlRnxyONJOerguQv1iwpyQZ9QXX4qy/XFQqXlf17XTsorxnHoJGbH/LXBvwtqDsF5A=="],
"@shikijs/transformers": ["@shikijs/transformers@3.12.2", "", { "dependencies": { "@shikijs/core": "3.12.2", "@shikijs/types": "3.12.2" } }, "sha512-+z1aMq4N5RoNGY8i7qnTYmG2MBYzFmwkm/yOd6cjEI7OVzcldVvzQCfxU1YbIVgsyB0xHVc2jFe1JhgoXyUoSQ=="],
"@shikijs/types": ["@shikijs/types@3.12.2", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-K5UIBzxCyv0YoxN3LMrKB9zuhp1bV+LgewxuVwHdl4Gz5oePoUFrr9EfgJlGlDeXCU1b/yhdnXeuRvAnz8HN8Q=="],
"@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="],
"@standard-schema/spec": ["@standard-schema/spec@1.0.0", "", {}, "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA=="],
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g=="],
"@tailwindcss/node": ["@tailwindcss/node@4.1.13", "", { "dependencies": { "@jridgewell/remapping": "^2.3.4", "enhanced-resolve": "^5.18.3", "jiti": "^2.5.1", "lightningcss": "1.30.1", "magic-string": "^0.30.18", "source-map-js": "^1.2.1", "tailwindcss": "4.1.13" } }, "sha512-eq3ouolC1oEFOAvOMOBAmfCIqZBJuvWvvYWh5h5iOYfe1HFC6+GZ6EIL0JdM3/niGRJmnrOc+8gl9/HGUaaptw=="],
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.1.13", "", { "dependencies": { "detect-libc": "^2.0.4", "tar": "^7.4.3" }, "optionalDependencies": { "@tailwindcss/oxide-android-arm64": "4.1.13", "@tailwindcss/oxide-darwin-arm64": "4.1.13", "@tailwindcss/oxide-darwin-x64": "4.1.13", "@tailwindcss/oxide-freebsd-x64": "4.1.13", "@tailwindcss/oxide-linux-arm-gnueabihf": "4.1.13", "@tailwindcss/oxide-linux-arm64-gnu": "4.1.13", "@tailwindcss/oxide-linux-arm64-musl": "4.1.13", "@tailwindcss/oxide-linux-x64-gnu": "4.1.13", "@tailwindcss/oxide-linux-x64-musl": "4.1.13", "@tailwindcss/oxide-wasm32-wasi": "4.1.13", "@tailwindcss/oxide-win32-arm64-msvc": "4.1.13", "@tailwindcss/oxide-win32-x64-msvc": "4.1.13" } }, "sha512-CPgsM1IpGRa880sMbYmG1s4xhAy3xEt1QULgTJGQmZUeNgXFR7s1YxYygmJyBGtou4SyEosGAGEeYqY7R53bIA=="],
"@tailwindcss/oxide-android-arm64": ["@tailwindcss/oxide-android-arm64@4.1.13", "", { "os": "android", "cpu": "arm64" }, "sha512-BrpTrVYyejbgGo57yc8ieE+D6VT9GOgnNdmh5Sac6+t0m+v+sKQevpFVpwX3pBrM2qKrQwJ0c5eDbtjouY/+ew=="],
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.1.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-YP+Jksc4U0KHcu76UhRDHq9bx4qtBftp9ShK/7UGfq0wpaP96YVnnjFnj3ZFrUAjc5iECzODl/Ts0AN7ZPOANQ=="],
"@tailwindcss/oxide-darwin-x64": ["@tailwindcss/oxide-darwin-x64@4.1.13", "", { "os": "darwin", "cpu": "x64" }, "sha512-aAJ3bbwrn/PQHDxCto9sxwQfT30PzyYJFG0u/BWZGeVXi5Hx6uuUOQEI2Fa43qvmUjTRQNZnGqe9t0Zntexeuw=="],
"@tailwindcss/oxide-freebsd-x64": ["@tailwindcss/oxide-freebsd-x64@4.1.13", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Wt8KvASHwSXhKE/dJLCCWcTSVmBj3xhVhp/aF3RpAhGeZ3sVo7+NTfgiN8Vey/Fi8prRClDs6/f0KXPDTZE6nQ=="],
"@tailwindcss/oxide-linux-arm-gnueabihf": ["@tailwindcss/oxide-linux-arm-gnueabihf@4.1.13", "", { "os": "linux", "cpu": "arm" }, "sha512-mbVbcAsW3Gkm2MGwA93eLtWrwajz91aXZCNSkGTx/R5eb6KpKD5q8Ueckkh9YNboU8RH7jiv+ol/I7ZyQ9H7Bw=="],
"@tailwindcss/oxide-linux-arm64-gnu": ["@tailwindcss/oxide-linux-arm64-gnu@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-wdtfkmpXiwej/yoAkrCP2DNzRXCALq9NVLgLELgLim1QpSfhQM5+ZxQQF8fkOiEpuNoKLp4nKZ6RC4kmeFH0HQ=="],
"@tailwindcss/oxide-linux-arm64-musl": ["@tailwindcss/oxide-linux-arm64-musl@4.1.13", "", { "os": "linux", "cpu": "arm64" }, "sha512-hZQrmtLdhyqzXHB7mkXfq0IYbxegaqTmfa1p9MBj72WPoDD3oNOh1Lnxf6xZLY9C3OV6qiCYkO1i/LrzEdW2mg=="],
"@tailwindcss/oxide-linux-x64-gnu": ["@tailwindcss/oxide-linux-x64-gnu@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-uaZTYWxSXyMWDJZNY1Ul7XkJTCBRFZ5Fo6wtjrgBKzZLoJNrG+WderJwAjPzuNZOnmdrVg260DKwXCFtJ/hWRQ=="],
"@tailwindcss/oxide-linux-x64-musl": ["@tailwindcss/oxide-linux-x64-musl@4.1.13", "", { "os": "linux", "cpu": "x64" }, "sha512-oXiPj5mi4Hdn50v5RdnuuIms0PVPI/EG4fxAfFiIKQh5TgQgX7oSuDWntHW7WNIi/yVLAiS+CRGW4RkoGSSgVQ=="],
"@tailwindcss/oxide-wasm32-wasi": ["@tailwindcss/oxide-wasm32-wasi@4.1.13", "", { "dependencies": { "@emnapi/core": "^1.4.5", "@emnapi/runtime": "^1.4.5", "@emnapi/wasi-threads": "^1.0.4", "@napi-rs/wasm-runtime": "^0.2.12", "@tybys/wasm-util": "^0.10.0", "tslib": "^2.8.0" }, "cpu": "none" }, "sha512-+LC2nNtPovtrDwBc/nqnIKYh/W2+R69FA0hgoeOn64BdCX522u19ryLh3Vf3F8W49XBcMIxSe665kwy21FkhvA=="],
"@tailwindcss/oxide-win32-arm64-msvc": ["@tailwindcss/oxide-win32-arm64-msvc@4.1.13", "", { "os": "win32", "cpu": "arm64" }, "sha512-dziTNeQXtoQ2KBXmrjCxsuPk3F3CQ/yb7ZNZNA+UkNTeiTGgfeh+gH5Pi7mRncVgcPD2xgHvkFCh/MhZWSgyQg=="],
"@tailwindcss/oxide-win32-x64-msvc": ["@tailwindcss/oxide-win32-x64-msvc@4.1.13", "", { "os": "win32", "cpu": "x64" }, "sha512-3+LKesjXydTkHk5zXX01b5KMzLV1xl2mcktBJkje7rhFUpUlYJy7IMOLqjIRQncLTa1WZZiFY/foAeB5nmaiTw=="],
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.1.13", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.1.13", "@tailwindcss/oxide": "4.1.13", "postcss": "^8.4.41", "tailwindcss": "4.1.13" } }, "sha512-HLgx6YSFKJT7rJqh9oJs/TkBFhxuMOfUKSBEPYwV+t78POOBsdQ7crhZLzwcH3T0UyUuOzU/GK5pk5eKr3wCiQ=="],
"@types/debug": ["@types/debug@4.1.12", "", { "dependencies": { "@types/ms": "*" } }, "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ=="],
"@types/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/estree-jsx": ["@types/estree-jsx@1.0.5", "", { "dependencies": { "@types/estree": "*" } }, "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg=="],
"@types/hast": ["@types/hast@3.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ=="],
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA=="],
"@types/mdast": ["@types/mdast@4.0.4", "", { "dependencies": { "@types/unist": "*" } }, "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA=="],
"@types/mdx": ["@types/mdx@2.0.13", "", {}, "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw=="],
"@types/ms": ["@types/ms@2.1.0", "", {}, "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA=="],
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
"@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="],
"@types/react-dom": ["@types/react-dom@19.1.9", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-qXRuZaOsAdXKFyOhRBg6Lqqc0yay13vN7KrIg4L7N4aaHN68ma9OK3NE1BoDFgFOTfM7zg+3/8+2n8rLUH3OKQ=="],
"@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="],
"@ungap/structured-clone": ["@ungap/structured-clone@1.3.0", "", {}, "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g=="],
"acorn": ["acorn@8.15.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
"ajv": ["ajv@8.17.1", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g=="],
"ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="],
"ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
"argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
"aria-hidden": ["aria-hidden@1.2.6", "", { "dependencies": { "tslib": "^2.0.0" } }, "sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA=="],
"astring": ["astring@1.9.0", "", { "bin": { "astring": "bin/astring" } }, "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg=="],
"bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="],
"caniuse-lite": ["caniuse-lite@1.0.30001741", "", {}, "sha512-QGUGitqsc8ARjLdgAfxETDhRbJ0REsP6O3I96TAth/mVjh2cYzN2u+3AzPP3aVSm2FehEItaJw1xd+IGBXWeSw=="],
"ccount": ["ccount@2.0.1", "", {}, "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg=="],
"character-entities": ["character-entities@2.0.2", "", {}, "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ=="],
"character-entities-html4": ["character-entities-html4@2.1.0", "", {}, "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA=="],
"character-entities-legacy": ["character-entities-legacy@3.0.0", "", {}, "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ=="],
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"class-variance-authority": ["class-variance-authority@0.7.1", "", { "dependencies": { "clsx": "^2.1.1" } }, "sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg=="],
"client-only": ["client-only@0.0.1", "", {}, "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA=="],
"clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="],
"collapse-white-space": ["collapse-white-space@2.1.0", "", {}, "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw=="],
"color": ["color@4.2.3", "", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"comma-separated-tokens": ["comma-separated-tokens@2.0.3", "", {}, "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg=="],
"compute-scroll-into-view": ["compute-scroll-into-view@3.1.1", "", {}, "sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"decode-named-character-reference": ["decode-named-character-reference@1.2.0", "", { "dependencies": { "character-entities": "^2.0.0" } }, "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q=="],
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"detect-node-es": ["detect-node-es@1.1.0", "", {}, "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ=="],
"devlop": ["devlop@1.1.0", "", { "dependencies": { "dequal": "^2.0.0" } }, "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA=="],
"enhanced-resolve": ["enhanced-resolve@5.18.3", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww=="],
"esast-util-from-estree": ["esast-util-from-estree@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "unist-util-position-from-estree": "^2.0.0" } }, "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ=="],
"esast-util-from-js": ["esast-util-from-js@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "acorn": "^8.0.0", "esast-util-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw=="],
"esbuild": ["esbuild@0.25.9", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.9", "@esbuild/android-arm": "0.25.9", "@esbuild/android-arm64": "0.25.9", "@esbuild/android-x64": "0.25.9", "@esbuild/darwin-arm64": "0.25.9", "@esbuild/darwin-x64": "0.25.9", "@esbuild/freebsd-arm64": "0.25.9", "@esbuild/freebsd-x64": "0.25.9", "@esbuild/linux-arm": "0.25.9", "@esbuild/linux-arm64": "0.25.9", "@esbuild/linux-ia32": "0.25.9", "@esbuild/linux-loong64": "0.25.9", "@esbuild/linux-mips64el": "0.25.9", "@esbuild/linux-ppc64": "0.25.9", "@esbuild/linux-riscv64": "0.25.9", "@esbuild/linux-s390x": "0.25.9", "@esbuild/linux-x64": "0.25.9", "@esbuild/netbsd-arm64": "0.25.9", "@esbuild/netbsd-x64": "0.25.9", "@esbuild/openbsd-arm64": "0.25.9", "@esbuild/openbsd-x64": "0.25.9", "@esbuild/openharmony-arm64": "0.25.9", "@esbuild/sunos-x64": "0.25.9", "@esbuild/win32-arm64": "0.25.9", "@esbuild/win32-ia32": "0.25.9", "@esbuild/win32-x64": "0.25.9" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-CRbODhYyQx3qp7ZEwzxOk4JBqmD/seJrzPa/cGjY1VtIn5E09Oi9/dB4JwctnfZ8Q8iT7rioVv5k/FNT/uf54g=="],
"escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="],
"esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
"estree-util-attach-comments": ["estree-util-attach-comments@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw=="],
"estree-util-build-jsx": ["estree-util-build-jsx@3.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "estree-walker": "^3.0.0" } }, "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ=="],
"estree-util-is-identifier-name": ["estree-util-is-identifier-name@3.0.0", "", {}, "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg=="],
"estree-util-scope": ["estree-util-scope@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0" } }, "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ=="],
"estree-util-to-js": ["estree-util-to-js@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "astring": "^1.8.0", "source-map": "^0.7.0" } }, "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg=="],
"estree-util-value-to-estree": ["estree-util-value-to-estree@3.4.0", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ=="],
"estree-util-visit": ["estree-util-visit@2.0.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/unist": "^3.0.0" } }, "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww=="],
"estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
"extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="],
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="],
"fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
"fast-xml-parser": ["fast-xml-parser@4.5.3", "", { "dependencies": { "strnum": "^1.1.1" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-RKihhV+SHsIUGXObeVy9AXiBbFwkVk7Syp8XgwN5U3JV416+Gwp/GO9i0JYKmikykgz/UHRrrV4ROuZEo/T0ig=="],
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
"foreach": ["foreach@2.0.6", "", {}, "sha512-k6GAGDyqLe9JaebCsFCoudPPWfihKu8pylYXRlqP1J7ms39iPoTtk2fviNglIeQEwdh0bQeKJ01ZPyuyQvKzwg=="],
"fumadocs-core": ["fumadocs-core@15.7.7", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.12", "@shikijs/rehype": "^3.12.0", "@shikijs/transformers": "^3.12.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "react-remove-scroll": "^2.7.1", "remark": "^15.0.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.12.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@mixedbread/sdk": "^0.19.0", "@oramacloud/client": "1.x.x || 2.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "react-router": "7.x.x" }, "optionalPeers": ["@mixedbread/sdk", "@oramacloud/client", "@types/react", "algoliasearch", "next", "react", "react-dom", "react-router"] }, "sha512-4mo8y1L2VV9TcrQ1gses3c5zzCaPwDPYjfrPET4Qf+m7GPOqZ7wiUeXMTYb98T+N5wS0G/fsr/xFPZkgwD44gQ=="],
"fumadocs-mdx": ["fumadocs-mdx@11.8.2", "", { "dependencies": { "@mdx-js/mdx": "^3.1.0", "@standard-schema/spec": "^1.0.0", "chokidar": "^4.0.3", "esbuild": "^0.25.9", "estree-util-value-to-estree": "^3.4.0", "js-yaml": "^4.1.0", "lru-cache": "^11.1.0", "picocolors": "^1.1.1", "remark-mdx": "^3.1.0", "remark-parse": "^11.0.0", "tinyexec": "^1.0.1", "tinyglobby": "^0.2.14", "unified": "^11.0.5", "unist-util-visit": "^5.0.0", "zod": "^4.1.4" }, "peerDependencies": { "@fumadocs/mdx-remote": "^1.4.0", "fumadocs-core": "^14.0.0 || ^15.0.0", "next": "^15.3.0", "react": "*", "vite": "6.x.x || 7.x.x" }, "optionalPeers": ["@fumadocs/mdx-remote", "next", "react", "vite"], "bin": { "fumadocs-mdx": "bin.js" } }, "sha512-omv2jNrdXfZiAC2p6YeO3y3GYyQesMRzvsCDdgf7L/MbPIfgvtPkmRG2kL42dFEVsi45zwj0UugX6AG5IhDuUQ=="],
"fumadocs-openapi": ["fumadocs-openapi@9.3.6", "", { "dependencies": { "@fumari/json-schema-to-typescript": "^1.1.3", "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-select": "^2.2.6", "@radix-ui/react-slot": "^1.2.3", "@scalar/openapi-parser": "0.18.3", "ajv": "^8.17.1", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.7.9", "fumadocs-ui": "15.7.9", "github-slugger": "^2.0.0", "gray-matter": "^4.0.3", "hast-util-to-jsx-runtime": "^2.3.6", "js-yaml": "^4.1.0", "next-themes": "^0.4.6", "openapi-sampler": "^1.6.1", "react-hook-form": "^7.62.0", "remark": "^15.0.1", "remark-rehype": "^11.1.2", "shiki": "^3.12.0", "tinyglobby": "^0.2.14", "xml-js": "^1.6.11" }, "peerDependencies": { "@scalar/api-client-react": "*", "@types/react": "*", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x" }, "optionalPeers": ["@scalar/api-client-react", "@types/react"] }, "sha512-ndgqMTBvdoE6GmYpELcdLbixopqC6396vk4/OVu2P/6gbScqy33mP+XpGMxw4JzvTW6/cuhkbc8XyC0VkqZWuA=="],
"fumadocs-ui": ["fumadocs-ui@15.7.7", "", { "dependencies": { "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.7.7", "lodash.merge": "^4.6.2", "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.0", "react-medium-image-zoom": "^5.3.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "tailwindcss": "^3.4.14 || ^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-nXeEnFI0h+JAbwWsKWcc6aBuR++jWlxhMpXQnPv4zbrrbds436lilrOu/xh5KxPiEe2M9HspKMN+Oee73jHQFw=="],
"get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="],
"github-slugger": ["github-slugger@2.0.0", "", {}, "sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="],
"hast-util-to-estree": ["hast-util-to-estree@3.1.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-attach-comments": "^3.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w=="],
"hast-util-to-html": ["hast-util-to-html@9.0.5", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-whitespace": "^3.0.0", "html-void-elements": "^3.0.0", "mdast-util-to-hast": "^13.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "stringify-entities": "^4.0.0", "zwitch": "^2.0.4" } }, "sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw=="],
"hast-util-to-jsx-runtime": ["hast-util-to-jsx-runtime@2.3.6", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "@types/unist": "^3.0.0", "comma-separated-tokens": "^2.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "hast-util-whitespace": "^3.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0", "style-to-js": "^1.0.0", "unist-util-position": "^5.0.0", "vfile-message": "^4.0.0" } }, "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg=="],
"hast-util-to-string": ["hast-util-to-string@3.0.1", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A=="],
"hast-util-whitespace": ["hast-util-whitespace@3.0.0", "", { "dependencies": { "@types/hast": "^3.0.0" } }, "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw=="],
"html-void-elements": ["html-void-elements@3.0.0", "", {}, "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg=="],
"image-size": ["image-size@2.0.2", "", { "bin": { "image-size": "bin/image-size.js" } }, "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w=="],
"inline-style-parser": ["inline-style-parser@0.2.4", "", {}, "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q=="],
"is-alphabetical": ["is-alphabetical@2.0.1", "", {}, "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ=="],
"is-alphanumerical": ["is-alphanumerical@2.0.1", "", { "dependencies": { "is-alphabetical": "^2.0.0", "is-decimal": "^2.0.0" } }, "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-decimal": ["is-decimal@2.0.1", "", {}, "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A=="],
"is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="],
"is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="],
"is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="],
"jiti": ["jiti@2.5.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-twQoecYPiVA5K/h6SxtORw/Bs3ar+mLUtoPSc7iMXzQzK8d7eJ/R09wmTwAjiamETn1cXYPGfNnu7DMoHgu12w=="],
"js-yaml": ["js-yaml@4.1.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA=="],
"json-pointer": ["json-pointer@0.6.2", "", { "dependencies": { "foreach": "^2.0.4" } }, "sha512-vLWcKbOaXlO+jvRy4qNd+TI1QUPZzfJj1tpJ3vAXDych5XJf93ftpUKe5pKCrzyIIwgBJcOcCVRUfqQP25afBw=="],
"json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
"jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="],
"kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="],
"leven": ["leven@4.0.0", "", {}, "sha512-puehA3YKku3osqPlNuzGDUHq8WpwXupUg1V6NXdV38G+gr+gkBwFC8g1b/+YcIvp8gnqVIus+eJCH/eGsRmJNw=="],
"lightningcss": ["lightningcss@1.30.1", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.30.1", "lightningcss-darwin-x64": "1.30.1", "lightningcss-freebsd-x64": "1.30.1", "lightningcss-linux-arm-gnueabihf": "1.30.1", "lightningcss-linux-arm64-gnu": "1.30.1", "lightningcss-linux-arm64-musl": "1.30.1", "lightningcss-linux-x64-gnu": "1.30.1", "lightningcss-linux-x64-musl": "1.30.1", "lightningcss-win32-arm64-msvc": "1.30.1", "lightningcss-win32-x64-msvc": "1.30.1" } }, "sha512-xi6IyHML+c9+Q3W0S4fCQJOym42pyurFiJUHEcEyHS0CeKzia4yZDEsLlqOFykxOdHpNy0NmvVO31vcSqAxJCg=="],
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.30.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-c8JK7hyE65X1MHMN+Viq9n11RRC7hgin3HhYKhrMyaXflk5GVplZ60IxyoVtzILeKr+xAJwg6zK6sjTBJ0FKYQ=="],
"lightningcss-darwin-x64": ["lightningcss-darwin-x64@1.30.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-k1EvjakfumAQoTfcXUcHQZhSpLlkAuEkdMBsI/ivWw9hL+7FtilQc0Cy3hrx0AAQrVtQAbMI7YjCgYgvn37PzA=="],
"lightningcss-freebsd-x64": ["lightningcss-freebsd-x64@1.30.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-kmW6UGCGg2PcyUE59K5r0kWfKPAVy4SltVeut+umLCFoJ53RdCUWxcRDzO1eTaxf/7Q2H7LTquFHPL5R+Gjyig=="],
"lightningcss-linux-arm-gnueabihf": ["lightningcss-linux-arm-gnueabihf@1.30.1", "", { "os": "linux", "cpu": "arm" }, "sha512-MjxUShl1v8pit+6D/zSPq9S9dQ2NPFSQwGvxBCYaBYLPlCWuPh9/t1MRS8iUaR8i+a6w7aps+B4N0S1TYP/R+Q=="],
"lightningcss-linux-arm64-gnu": ["lightningcss-linux-arm64-gnu@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-gB72maP8rmrKsnKYy8XUuXi/4OctJiuQjcuqWNlJQ6jZiWqtPvqFziskH3hnajfvKB27ynbVCucKSm2rkQp4Bw=="],
"lightningcss-linux-arm64-musl": ["lightningcss-linux-arm64-musl@1.30.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-jmUQVx4331m6LIX+0wUhBbmMX7TCfjF5FoOH6SD1CttzuYlGNVpA7QnrmLxrsub43ClTINfGSYyHe2HWeLl5CQ=="],
"lightningcss-linux-x64-gnu": ["lightningcss-linux-x64-gnu@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-piWx3z4wN8J8z3+O5kO74+yr6ze/dKmPnI7vLqfSqI8bccaTGY5xiSGVIJBDd5K5BHlvVLpUB3S2YCfelyJ1bw=="],
"lightningcss-linux-x64-musl": ["lightningcss-linux-x64-musl@1.30.1", "", { "os": "linux", "cpu": "x64" }, "sha512-rRomAK7eIkL+tHY0YPxbc5Dra2gXlI63HL+v1Pdi1a3sC+tJTcFrHX+E86sulgAXeI7rSzDYhPSeHHjqFhqfeQ=="],
"lightningcss-win32-arm64-msvc": ["lightningcss-win32-arm64-msvc@1.30.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-mSL4rqPi4iXq5YVqzSsJgMVFENoa4nGTT/GjO2c0Yl9OuQfPsIfncvLrEW6RbbB24WtZ3xP/2CCmI3tNkNV4oA=="],
"lightningcss-win32-x64-msvc": ["lightningcss-win32-x64-msvc@1.30.1", "", { "os": "win32", "cpu": "x64" }, "sha512-PVqXh48wh4T53F/1CCu8PIPCxLzWyCnn/9T5W1Jpmdy5h9Cwd+0YQS6/LwhHXSafuc61/xg9Lv5OrCby6a++jg=="],
"lodash.merge": ["lodash.merge@4.6.2", "", {}, "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ=="],
"longest-streak": ["longest-streak@3.1.0", "", {}, "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g=="],
"lru-cache": ["lru-cache@11.2.1", "", {}, "sha512-r8LA6i4LP4EeWOhqBaZZjDWwehd1xUJPCJd9Sv300H0ZmcUER4+JPh7bqqZeqs1o5pgtgvXm+d9UGrB5zZGDiQ=="],
"lucide-react": ["lucide-react@0.542.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-w3hD8/SQB7+lzU2r4VdFyzzOzKnUjTZIF/MQJGSSvni7Llewni4vuViRppfRAa2guOsY5k4jZyxw/i9DQHv+dw=="],
"magic-string": ["magic-string@0.30.18", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-yi8swmWbO17qHhwIBNeeZxTceJMeBvWJaId6dyvTSOwTipqeHhMhOrz6513r1sOKnpvQ7zkhlG8tPrpilwTxHQ=="],
"markdown-extensions": ["markdown-extensions@2.0.0", "", {}, "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q=="],
"markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="],
"mdast-util-find-and-replace": ["mdast-util-find-and-replace@3.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "escape-string-regexp": "^5.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg=="],
"mdast-util-from-markdown": ["mdast-util-from-markdown@2.0.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "mdast-util-to-string": "^4.0.0", "micromark": "^4.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA=="],
"mdast-util-gfm": ["mdast-util-gfm@3.1.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-gfm-autolink-literal": "^2.0.0", "mdast-util-gfm-footnote": "^2.0.0", "mdast-util-gfm-strikethrough": "^2.0.0", "mdast-util-gfm-table": "^2.0.0", "mdast-util-gfm-task-list-item": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ=="],
"mdast-util-gfm-autolink-literal": ["mdast-util-gfm-autolink-literal@2.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "ccount": "^2.0.0", "devlop": "^1.0.0", "mdast-util-find-and-replace": "^3.0.0", "micromark-util-character": "^2.0.0" } }, "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ=="],
"mdast-util-gfm-footnote": ["mdast-util-gfm-footnote@2.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0" } }, "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ=="],
"mdast-util-gfm-strikethrough": ["mdast-util-gfm-strikethrough@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg=="],
"mdast-util-gfm-table": ["mdast-util-gfm-table@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "markdown-table": "^3.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg=="],
"mdast-util-gfm-task-list-item": ["mdast-util-gfm-task-list-item@2.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ=="],
"mdast-util-mdx": ["mdast-util-mdx@3.0.0", "", { "dependencies": { "mdast-util-from-markdown": "^2.0.0", "mdast-util-mdx-expression": "^2.0.0", "mdast-util-mdx-jsx": "^3.0.0", "mdast-util-mdxjs-esm": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w=="],
"mdast-util-mdx-expression": ["mdast-util-mdx-expression@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ=="],
"mdast-util-mdx-jsx": ["mdast-util-mdx-jsx@3.2.0", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "ccount": "^2.0.0", "devlop": "^1.1.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-stringify-position": "^4.0.0", "vfile-message": "^4.0.0" } }, "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q=="],
"mdast-util-mdxjs-esm": ["mdast-util-mdxjs-esm@2.0.1", "", { "dependencies": { "@types/estree-jsx": "^1.0.0", "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0" } }, "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg=="],
"mdast-util-phrasing": ["mdast-util-phrasing@4.1.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "unist-util-is": "^6.0.0" } }, "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w=="],
"mdast-util-to-hast": ["mdast-util-to-hast@13.2.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "@ungap/structured-clone": "^1.0.0", "devlop": "^1.0.0", "micromark-util-sanitize-uri": "^2.0.0", "trim-lines": "^3.0.0", "unist-util-position": "^5.0.0", "unist-util-visit": "^5.0.0", "vfile": "^6.0.0" } }, "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA=="],
"mdast-util-to-markdown": ["mdast-util-to-markdown@2.1.2", "", { "dependencies": { "@types/mdast": "^4.0.0", "@types/unist": "^3.0.0", "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" } }, "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA=="],
"mdast-util-to-string": ["mdast-util-to-string@4.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0" } }, "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg=="],
"micromark": ["micromark@4.0.2", "", { "dependencies": { "@types/debug": "^4.0.0", "debug": "^4.0.0", "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA=="],
"micromark-core-commonmark": ["micromark-core-commonmark@2.0.3", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg=="],
"micromark-extension-gfm": ["micromark-extension-gfm@3.0.0", "", { "dependencies": { "micromark-extension-gfm-autolink-literal": "^2.0.0", "micromark-extension-gfm-footnote": "^2.0.0", "micromark-extension-gfm-strikethrough": "^2.0.0", "micromark-extension-gfm-table": "^2.0.0", "micromark-extension-gfm-tagfilter": "^2.0.0", "micromark-extension-gfm-task-list-item": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w=="],
"micromark-extension-gfm-autolink-literal": ["micromark-extension-gfm-autolink-literal@2.1.0", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw=="],
"micromark-extension-gfm-footnote": ["micromark-extension-gfm-footnote@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-sanitize-uri": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw=="],
"micromark-extension-gfm-strikethrough": ["micromark-extension-gfm-strikethrough@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw=="],
"micromark-extension-gfm-table": ["micromark-extension-gfm-table@2.1.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg=="],
"micromark-extension-gfm-tagfilter": ["micromark-extension-gfm-tagfilter@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg=="],
"micromark-extension-gfm-task-list-item": ["micromark-extension-gfm-task-list-item@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw=="],
"micromark-extension-mdx-expression": ["micromark-extension-mdx-expression@3.0.1", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q=="],
"micromark-extension-mdx-jsx": ["micromark-extension-mdx-jsx@3.0.2", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "estree-util-is-identifier-name": "^3.0.0", "micromark-factory-mdx-expression": "^2.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ=="],
"micromark-extension-mdx-md": ["micromark-extension-mdx-md@2.0.0", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ=="],
"micromark-extension-mdxjs": ["micromark-extension-mdxjs@3.0.0", "", { "dependencies": { "acorn": "^8.0.0", "acorn-jsx": "^5.0.0", "micromark-extension-mdx-expression": "^3.0.0", "micromark-extension-mdx-jsx": "^3.0.0", "micromark-extension-mdx-md": "^2.0.0", "micromark-extension-mdxjs-esm": "^3.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ=="],
"micromark-extension-mdxjs-esm": ["micromark-extension-mdxjs-esm@3.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-core-commonmark": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A=="],
"micromark-factory-destination": ["micromark-factory-destination@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA=="],
"micromark-factory-label": ["micromark-factory-label@2.0.1", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg=="],
"micromark-factory-mdx-expression": ["micromark-factory-mdx-expression@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "devlop": "^1.0.0", "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-events-to-acorn": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "unist-util-position-from-estree": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ=="],
"micromark-factory-space": ["micromark-factory-space@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg=="],
"micromark-factory-title": ["micromark-factory-title@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw=="],
"micromark-factory-whitespace": ["micromark-factory-whitespace@2.0.1", "", { "dependencies": { "micromark-factory-space": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ=="],
"micromark-util-character": ["micromark-util-character@2.1.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q=="],
"micromark-util-chunked": ["micromark-util-chunked@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA=="],
"micromark-util-classify-character": ["micromark-util-classify-character@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q=="],
"micromark-util-combine-extensions": ["micromark-util-combine-extensions@2.0.1", "", { "dependencies": { "micromark-util-chunked": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg=="],
"micromark-util-decode-numeric-character-reference": ["micromark-util-decode-numeric-character-reference@2.0.2", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw=="],
"micromark-util-decode-string": ["micromark-util-decode-string@2.0.1", "", { "dependencies": { "decode-named-character-reference": "^1.0.0", "micromark-util-character": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ=="],
"micromark-util-encode": ["micromark-util-encode@2.0.1", "", {}, "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw=="],
"micromark-util-events-to-acorn": ["micromark-util-events-to-acorn@2.0.3", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/unist": "^3.0.0", "devlop": "^1.0.0", "estree-util-visit": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0", "vfile-message": "^4.0.0" } }, "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg=="],
"micromark-util-html-tag-name": ["micromark-util-html-tag-name@2.0.1", "", {}, "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA=="],
"micromark-util-normalize-identifier": ["micromark-util-normalize-identifier@2.0.1", "", { "dependencies": { "micromark-util-symbol": "^2.0.0" } }, "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q=="],
"micromark-util-resolve-all": ["micromark-util-resolve-all@2.0.1", "", { "dependencies": { "micromark-util-types": "^2.0.0" } }, "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg=="],
"micromark-util-sanitize-uri": ["micromark-util-sanitize-uri@2.0.1", "", { "dependencies": { "micromark-util-character": "^2.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-symbol": "^2.0.0" } }, "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ=="],
"micromark-util-subtokenize": ["micromark-util-subtokenize@2.1.0", "", { "dependencies": { "devlop": "^1.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-symbol": "^2.0.0", "micromark-util-types": "^2.0.0" } }, "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA=="],
"micromark-util-symbol": ["micromark-util-symbol@2.0.1", "", {}, "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q=="],
"micromark-util-types": ["micromark-util-types@2.0.2", "", {}, "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA=="],
"minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="],
"minizlib": ["minizlib@3.0.2", "", { "dependencies": { "minipass": "^7.1.2" } }, "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA=="],
"mkdirp": ["mkdirp@3.0.1", "", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"next": ["next@15.5.2", "", { "dependencies": { "@next/env": "15.5.2", "@swc/helpers": "0.5.15", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "15.5.2", "@next/swc-darwin-x64": "15.5.2", "@next/swc-linux-arm64-gnu": "15.5.2", "@next/swc-linux-arm64-musl": "15.5.2", "@next/swc-linux-x64-gnu": "15.5.2", "@next/swc-linux-x64-musl": "15.5.2", "@next/swc-win32-arm64-msvc": "15.5.2", "@next/swc-win32-x64-msvc": "15.5.2", "sharp": "^0.34.3" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": { "next": "dist/bin/next" } }, "sha512-H8Otr7abj1glFhbGnvUt3gz++0AF1+QoCXEBmd/6aKbfdFwrn0LpA836Ed5+00va/7HQSDD+mOoVhn3tNy3e/Q=="],
"next-themes": ["next-themes@0.4.6", "", { "peerDependencies": { "react": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom": "^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc" } }, "sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA=="],
"npm-to-yarn": ["npm-to-yarn@3.0.1", "", {}, "sha512-tt6PvKu4WyzPwWUzy/hvPFqn+uwXO0K1ZHka8az3NnrhWJDmSqI8ncWq0fkL0k/lmmi5tAC11FXwXuh0rFbt1A=="],
"oniguruma-parser": ["oniguruma-parser@0.12.1", "", {}, "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w=="],
"oniguruma-to-es": ["oniguruma-to-es@4.3.3", "", { "dependencies": { "oniguruma-parser": "^0.12.1", "regex": "^6.0.1", "regex-recursion": "^6.0.2" } }, "sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg=="],
"openapi-sampler": ["openapi-sampler@1.6.1", "", { "dependencies": { "@types/json-schema": "^7.0.7", "fast-xml-parser": "^4.5.0", "json-pointer": "0.6.2" } }, "sha512-s1cIatOqrrhSj2tmJ4abFYZQK6l5v+V4toO5q1Pa0DyN8mtyqy2I+Qrj5W9vOELEtybIMQs/TBZGVO/DtTFK8w=="],
"parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.3", "", {}, "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.0", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA=="],
"prettier": ["prettier@3.6.2", "", { "bin": { "prettier": "bin/prettier.cjs" } }, "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ=="],
"property-information": ["property-information@7.1.0", "", {}, "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ=="],
"react": ["react@19.1.1", "", {}, "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ=="],
"react-dom": ["react-dom@19.1.1", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.1" } }, "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw=="],
"react-hook-form": ["react-hook-form@7.62.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17 || ^18 || ^19" } }, "sha512-7KWFejc98xqG/F4bAxpL41NB3o1nnvQO1RWZT3TqRZYL8RryQETGfEdVnJN2fy1crCiBLLjkRBVK05j24FxJGA=="],
"react-medium-image-zoom": ["react-medium-image-zoom@5.3.0", "", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-RCIzVlsKqy3BYgGgYbolUfuvx0aSKC7YhX/IJGEp+WJxsqdIVYJHkBdj++FAj6VD7RiWj6VVmdCfa/9vJE9hZg=="],
"react-remove-scroll": ["react-remove-scroll@2.7.1", "", { "dependencies": { "react-remove-scroll-bar": "^2.3.7", "react-style-singleton": "^2.2.3", "tslib": "^2.1.0", "use-callback-ref": "^1.3.3", "use-sidecar": "^1.1.3" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-HpMh8+oahmIdOuS5aFKKY6Pyog+FNaZV/XyJOq7b4YFwsFHe5yYfdbIalI4k3vU2nSDql7YskmUseHsRrJqIPA=="],
"react-remove-scroll-bar": ["react-remove-scroll-bar@2.3.8", "", { "dependencies": { "react-style-singleton": "^2.2.2", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react"] }, "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q=="],
"react-style-singleton": ["react-style-singleton@2.2.3", "", { "dependencies": { "get-nonce": "^1.0.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ=="],
"readdirp": ["readdirp@4.1.2", "", {}, "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg=="],
"recma-build-jsx": ["recma-build-jsx@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-build-jsx": "^3.0.0", "vfile": "^6.0.0" } }, "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew=="],
"recma-jsx": ["recma-jsx@1.0.1", "", { "dependencies": { "acorn-jsx": "^5.0.0", "estree-util-to-js": "^2.0.0", "recma-parse": "^1.0.0", "recma-stringify": "^1.0.0", "unified": "^11.0.0" }, "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w=="],
"recma-parse": ["recma-parse@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "esast-util-from-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ=="],
"recma-stringify": ["recma-stringify@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "estree-util-to-js": "^2.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g=="],
"regex": ["regex@6.0.1", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA=="],
"regex-recursion": ["regex-recursion@6.0.2", "", { "dependencies": { "regex-utilities": "^2.3.0" } }, "sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg=="],
"regex-utilities": ["regex-utilities@2.3.0", "", {}, "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng=="],
"rehype-recma": ["rehype-recma@1.0.0", "", { "dependencies": { "@types/estree": "^1.0.0", "@types/hast": "^3.0.0", "hast-util-to-estree": "^3.0.0" } }, "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw=="],
"remark": ["remark@15.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-Eht5w30ruCXgFmxVUSlNWQ9iiimq07URKeFS3hNc8cUWy1llX4KDWfyEDZRycMc+znsN9Ux5/tJ/BFdgdOwA3A=="],
"remark-gfm": ["remark-gfm@4.0.1", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-gfm": "^3.0.0", "micromark-extension-gfm": "^3.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "unified": "^11.0.0" } }, "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg=="],
"remark-mdx": ["remark-mdx@3.1.1", "", { "dependencies": { "mdast-util-mdx": "^3.0.0", "micromark-extension-mdxjs": "^3.0.0" } }, "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg=="],
"remark-parse": ["remark-parse@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-from-markdown": "^2.0.0", "micromark-util-types": "^2.0.0", "unified": "^11.0.0" } }, "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA=="],
"remark-rehype": ["remark-rehype@11.1.2", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/mdast": "^4.0.0", "mdast-util-to-hast": "^13.0.0", "unified": "^11.0.0", "vfile": "^6.0.0" } }, "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw=="],
"remark-stringify": ["remark-stringify@11.0.0", "", { "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-to-markdown": "^2.0.0", "unified": "^11.0.0" } }, "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw=="],
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
"sax": ["sax@1.4.1", "", {}, "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"scroll-into-view-if-needed": ["scroll-into-view-if-needed@3.1.0", "", { "dependencies": { "compute-scroll-into-view": "^3.0.2" } }, "sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ=="],
"section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"sharp": ["sharp@0.34.3", "", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.4", "semver": "^7.7.2" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.3", "@img/sharp-darwin-x64": "0.34.3", "@img/sharp-libvips-darwin-arm64": "1.2.0", "@img/sharp-libvips-darwin-x64": "1.2.0", "@img/sharp-libvips-linux-arm": "1.2.0", "@img/sharp-libvips-linux-arm64": "1.2.0", "@img/sharp-libvips-linux-ppc64": "1.2.0", "@img/sharp-libvips-linux-s390x": "1.2.0", "@img/sharp-libvips-linux-x64": "1.2.0", "@img/sharp-libvips-linuxmusl-arm64": "1.2.0", "@img/sharp-libvips-linuxmusl-x64": "1.2.0", "@img/sharp-linux-arm": "0.34.3", "@img/sharp-linux-arm64": "0.34.3", "@img/sharp-linux-ppc64": "0.34.3", "@img/sharp-linux-s390x": "0.34.3", "@img/sharp-linux-x64": "0.34.3", "@img/sharp-linuxmusl-arm64": "0.34.3", "@img/sharp-linuxmusl-x64": "0.34.3", "@img/sharp-wasm32": "0.34.3", "@img/sharp-win32-arm64": "0.34.3", "@img/sharp-win32-ia32": "0.34.3", "@img/sharp-win32-x64": "0.34.3" } }, "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg=="],
"shiki": ["shiki@3.12.2", "", { "dependencies": { "@shikijs/core": "3.12.2", "@shikijs/engine-javascript": "3.12.2", "@shikijs/engine-oniguruma": "3.12.2", "@shikijs/langs": "3.12.2", "@shikijs/themes": "3.12.2", "@shikijs/types": "3.12.2", "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-uIrKI+f9IPz1zDT+GMz+0RjzKJiijVr6WDWm9Pe3NNY6QigKCfifCEv9v9R2mDASKKjzjQ2QpFLcxaR3iHSnMA=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
"source-map": ["source-map@0.7.6", "", {}, "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"space-separated-tokens": ["space-separated-tokens@2.0.2", "", {}, "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q=="],
"sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="],
"stringify-entities": ["stringify-entities@4.0.4", "", { "dependencies": { "character-entities-html4": "^2.0.0", "character-entities-legacy": "^3.0.0" } }, "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg=="],
"strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="],
"strnum": ["strnum@1.1.2", "", {}, "sha512-vrN+B7DBIoTTZjnPNewwhx6cBA/H+IS7rfW68n7XxC1y7uoiGQBxaKzqucGUgavX15dJgiGztLJ8vxuEzwqBdA=="],
"style-to-js": ["style-to-js@1.1.17", "", { "dependencies": { "style-to-object": "1.0.9" } }, "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA=="],
"style-to-object": ["style-to-object@1.0.9", "", { "dependencies": { "inline-style-parser": "0.2.4" } }, "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw=="],
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="],
"tailwind-merge": ["tailwind-merge@3.3.1", "", {}, "sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g=="],
"tailwindcss": ["tailwindcss@4.1.13", "", {}, "sha512-i+zidfmTqtwquj4hMEwdjshYYgMbOrPzb9a0M3ZgNa0JMoZeFC6bxZvO8yr8ozS6ix2SDz0+mvryPeBs2TFE+w=="],
"tapable": ["tapable@2.2.3", "", {}, "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg=="],
"tar": ["tar@7.4.3", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.0.1", "mkdirp": "^3.0.1", "yallist": "^5.0.0" } }, "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw=="],
"tinyexec": ["tinyexec@1.0.1", "", {}, "sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="],
"trough": ["trough@2.2.0", "", {}, "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw=="],
"tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
"unified": ["unified@11.0.5", "", { "dependencies": { "@types/unist": "^3.0.0", "bail": "^2.0.0", "devlop": "^1.0.0", "extend": "^3.0.0", "is-plain-obj": "^4.0.0", "trough": "^2.0.0", "vfile": "^6.0.0" } }, "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA=="],
"unist-util-is": ["unist-util-is@6.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw=="],
"unist-util-position": ["unist-util-position@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA=="],
"unist-util-position-from-estree": ["unist-util-position-from-estree@2.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ=="],
"unist-util-stringify-position": ["unist-util-stringify-position@4.0.0", "", { "dependencies": { "@types/unist": "^3.0.0" } }, "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ=="],
"unist-util-visit": ["unist-util-visit@5.0.0", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0", "unist-util-visit-parents": "^6.0.0" } }, "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg=="],
"unist-util-visit-parents": ["unist-util-visit-parents@6.0.1", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-is": "^6.0.0" } }, "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw=="],
"use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="],
"use-sidecar": ["use-sidecar@1.1.3", "", { "dependencies": { "detect-node-es": "^1.1.0", "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"vfile": ["vfile@6.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "vfile-message": "^4.0.0" } }, "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q=="],
"vfile-message": ["vfile-message@4.0.3", "", { "dependencies": { "@types/unist": "^3.0.0", "unist-util-stringify-position": "^4.0.0" } }, "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw=="],
"xml-js": ["xml-js@1.6.11", "", { "dependencies": { "sax": "^1.2.4" }, "bin": { "xml-js": "./bin/cli.js" } }, "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g=="],
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
"yaml": ["yaml@2.8.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ=="],
"zod": ["zod@4.1.5", "", {}, "sha512-rcUUZqlLJgBC33IT3PNMgsCq6TzLQEG/Ei/KTCU0PedSWRMAXoOUN+4t/0H+Q8bdnLPdqUYnvboJT0bn/229qg=="],
"zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="],
"@scalar/openapi-types/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.5.0", "", { "dependencies": { "@emnapi/wasi-threads": "1.1.0", "tslib": "^2.4.0" }, "bundled": true }, "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/runtime": ["@emnapi/runtime@1.5.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ=="],
"@tailwindcss/oxide-wasm32-wasi/@emnapi/wasi-threads": ["@emnapi/wasi-threads@1.1.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ=="],
"@tailwindcss/oxide-wasm32-wasi/@napi-rs/wasm-runtime": ["@napi-rs/wasm-runtime@0.2.12", "", { "dependencies": { "@emnapi/core": "^1.4.3", "@emnapi/runtime": "^1.4.3", "@tybys/wasm-util": "^0.10.0" }, "bundled": true }, "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ=="],
"@tailwindcss/oxide-wasm32-wasi/@tybys/wasm-util": ["@tybys/wasm-util@0.10.0", "", { "dependencies": { "tslib": "^2.4.0" }, "bundled": true }, "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ=="],
"@tailwindcss/oxide-wasm32-wasi/tslib": ["tslib@2.8.1", "", { "bundled": true }, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
"fumadocs-openapi/fumadocs-core": ["fumadocs-core@15.7.9", "", { "dependencies": { "@formatjs/intl-localematcher": "^0.6.1", "@orama/orama": "^3.1.12", "@shikijs/rehype": "^3.12.0", "@shikijs/transformers": "^3.12.0", "github-slugger": "^2.0.0", "hast-util-to-estree": "^3.1.3", "hast-util-to-jsx-runtime": "^2.3.6", "image-size": "^2.0.2", "negotiator": "^1.0.0", "npm-to-yarn": "^3.0.1", "react-remove-scroll": "^2.7.1", "remark": "^15.0.0", "remark-gfm": "^4.0.1", "remark-rehype": "^11.1.2", "scroll-into-view-if-needed": "^3.1.0", "shiki": "^3.12.0", "unist-util-visit": "^5.0.0" }, "peerDependencies": { "@mixedbread/sdk": "^0.19.0", "@oramacloud/client": "1.x.x || 2.x.x", "@tanstack/react-router": "1.x.x", "@types/react": "*", "algoliasearch": "5.x.x", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "react-router": "7.x.x", "waku": "^0.26.0" }, "optionalPeers": ["@mixedbread/sdk", "@oramacloud/client", "@tanstack/react-router", "@types/react", "algoliasearch", "next", "react", "react-dom", "react-router", "waku"] }, "sha512-GaD8UXcpJgp0CTXVd+sKL1Qt+tCN5cQsBxOXneGpEzoida4MRS83IphodLsvMqz33uVHJY+J22g8ijTnpgWj4A=="],
"fumadocs-openapi/fumadocs-ui": ["fumadocs-ui@15.7.9", "", { "dependencies": { "@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-collapsible": "^1.1.12", "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-direction": "^1.1.1", "@radix-ui/react-navigation-menu": "^1.2.14", "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-presence": "^1.1.5", "@radix-ui/react-scroll-area": "^1.2.10", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tabs": "^1.1.13", "class-variance-authority": "^0.7.1", "fumadocs-core": "15.7.9", "lodash.merge": "^4.6.2", "next-themes": "^0.4.6", "postcss-selector-parser": "^7.1.0", "react-medium-image-zoom": "^5.3.0", "scroll-into-view-if-needed": "^3.1.0", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@types/react": "*", "next": "14.x.x || 15.x.x", "react": "18.x.x || 19.x.x", "react-dom": "18.x.x || 19.x.x", "tailwindcss": "^3.4.14 || ^4.0.0" }, "optionalPeers": ["@types/react", "next", "tailwindcss"] }, "sha512-6ZOniTWBRB9DSBV6ae2hX+wYNhkzosqN4N2kT2RUGsp2MuPr+pLyKazbUsOj+pSat1BuDZpkBtBa2ht1ky7L2g=="],
"gray-matter/js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="],
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ=="],
"parse-entities/@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="],
"gray-matter/js-yaml/argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="],
}
}

View File

@@ -1,43 +0,0 @@
---
title: 后端开发
description: OpenIsle 后端服务开发指南
---
# 后端开发
OpenIsle 后端基于 Spring Boot 框架构建,提供完整的 RESTful API 服务。
## 技术栈
- **Spring Boot** - 主框架
- **Spring Security** - 安全认证
- **MyBatis** - 数据访问层
- **MySQL** - 数据库
- **Redis** - 缓存和会话存储
## 项目结构
```
backend/
├── src/main/java/com/
│ ├── controller/ # 控制器层
│ ├── service/ # 业务逻辑层
│ ├── mapper/ # 数据访问层
│ ├── entity/ # 实体类
│ └── config/ # 配置类
└── src/main/resources/
├── application.properties
└── mapper/ # MyBatis 映射文件
```
## 开发环境设置
1. 安装 Java 17+
2. 安装 MySQL 8.0+
3. 安装 Redis
4. 配置数据库连接
5. 启动应用
## API 接口
详细的 API 接口文档请查看 [API 文档](/openapi)。

View File

@@ -1,3 +0,0 @@
{
"root": true
}

View File

@@ -1,53 +0,0 @@
---
title: 前端开发
description: OpenIsle 前端应用开发指南
---
# 前端开发
OpenIsle 前端基于 Nuxt.js 框架构建,提供现代化的用户界面和交互体验。
## 技术栈
- **Nuxt.js** - Vue.js 全栈框架
- **Vue 3** - 前端框架
- **TypeScript** - 类型安全
- **Tailwind CSS** - 样式框架
- **Pinia** - 状态管理
## 项目结构
```
frontend_nuxt/
├── components/ # Vue 组件
├── pages/ # 页面路由
├── composables/ # 组合式函数
├── plugins/ # 插件配置
├── utils/ # 工具函数
└── assets/ # 静态资源
```
## 开发环境设置
1. 安装 Node.js 18+
2. 安装依赖:`npm install`
3. 启动开发服务器:`npm run dev`
4. 访问 http://localhost:3000
## 主要功能
- 用户认证和授权
- 文章发布和管理
- 评论和互动
- 实时消息
- 用户个人资料
- 管理后台
## 组件库
项目使用自定义组件库,主要组件包括:
- `BaseInput` - 输入框组件
- `BasePopup` - 弹窗组件
- `PostEditor` - 文章编辑器
- `CommentEditor` - 评论编辑器

View File

@@ -1,3 +0,0 @@
{
"root": true
}

View File

@@ -1,14 +0,0 @@
---
title: 欢迎使用 OpenIsle
description: OpenIsle 文档首页
---
# 欢迎使用 OpenIsle
OpenIsle 是一个现代化的社区平台,提供完整的社交功能。
## 快速开始
- [后端开发指南](/backend) - 了解后端架构和开发
- [前端开发指南](/frontend) - 了解前端技术栈和组件
- [API 文档](/openapi) - 查看完整的 API 接口文档

View File

@@ -1,3 +0,0 @@
{
"pages": ["index", "frontend", "backend", "openapi"]
}

View File

@@ -1 +0,0 @@
(generated)/

View File

@@ -1,4 +0,0 @@
---
title: API 概览
description: Open API 接口文档
---

View File

@@ -1,4 +0,0 @@
{
"pages": ["index", "...(generated)"],
"root": true
}

View File

@@ -1,19 +0,0 @@
import type { BaseLayoutProps } from 'fumadocs-ui/layouts/shared';
/**
* Shared layout configurations
*/
export function baseOptions(): BaseLayoutProps {
return {
githubUrl: 'https://github.com/nagisa77/OpenIsle',
nav: {
title: 'OpenIsle Docs',
url: '/',
},
searchToggle: {
enabled: false,
},
// see https://fumadocs.dev/docs/ui/navigation/links
links: [],
};
}

View File

@@ -1,4 +0,0 @@
'use client';
// forward them so that Fumadocs can also use your media adapter in a client component
export { OpenIsleMediaAdapter } from './media-adapter';

View File

@@ -1,23 +0,0 @@
import type { MediaAdapter } from 'fumadocs-openapi';
export const OpenIsleMediaAdapter: MediaAdapter = {
encode(data) {
return JSON.stringify(data.body);
},
// returns code that inits a `body` variable, used for request body
generateExample(data, ctx) {
if (ctx.lang === 'js') {
return `const body = "hello world"`;
}
if (ctx.lang === 'python') {
return `body = "hello world"`;
}
if (ctx.lang === 'go' && 'addImport' in ctx) {
ctx.addImport('strings');
return `body := strings.NewReader("hello world")`;
}
},
};

Some files were not shown because too many files have changed in this diff Show More