diff --git a/backend/src/main/java/com/openisle/controller/ActivityController.java b/backend/src/main/java/com/openisle/controller/ActivityController.java index 4cbe4db0f..5aaf426e0 100644 --- a/backend/src/main/java/com/openisle/controller/ActivityController.java +++ b/backend/src/main/java/com/openisle/controller/ActivityController.java @@ -12,6 +12,12 @@ import com.openisle.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; 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.stream.Collectors; @@ -25,6 +31,9 @@ public class ActivityController { private final ActivityMapper activityMapper; @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 list() { return activityService.list().stream() .map(activityMapper::toDto) @@ -32,6 +41,9 @@ public class ActivityController { } @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() { Activity a = activityService.getByType(ActivityType.MILK_TEA); long count = activityService.countParticipants(a); @@ -45,6 +57,10 @@ public class ActivityController { } @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 redeemMilkTea(@RequestBody MilkTeaRedeemRequest req, Authentication auth) { User user = userService.findByIdentifier(auth.getName()).orElseThrow(); Activity a = activityService.getByType(ActivityType.MILK_TEA); diff --git a/backend/src/main/java/com/openisle/controller/AdminCommentController.java b/backend/src/main/java/com/openisle/controller/AdminCommentController.java index 850b81784..1f57be138 100644 --- a/backend/src/main/java/com/openisle/controller/AdminCommentController.java +++ b/backend/src/main/java/com/openisle/controller/AdminCommentController.java @@ -3,6 +3,11 @@ package com.openisle.controller; import com.openisle.dto.CommentDto; import com.openisle.mapper.CommentMapper; 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 org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @@ -18,11 +23,19 @@ public class AdminCommentController { private final CommentMapper commentMapper; @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) { return commentMapper.toDto(commentService.pinComment(auth.getName(), id)); } @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) { return commentMapper.toDto(commentService.unpinComment(auth.getName(), id)); } diff --git a/backend/src/main/java/com/openisle/controller/AdminConfigController.java b/backend/src/main/java/com/openisle/controller/AdminConfigController.java index cf3e7c7d6..b61954d3d 100644 --- a/backend/src/main/java/com/openisle/controller/AdminConfigController.java +++ b/backend/src/main/java/com/openisle/controller/AdminConfigController.java @@ -5,6 +5,11 @@ import com.openisle.service.AiUsageService; import com.openisle.service.PasswordValidator; import com.openisle.service.PostService; 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 org.springframework.web.bind.annotation.*; @@ -18,6 +23,10 @@ public class AdminConfigController { private final RegisterModeService registerModeService; @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() { ConfigDto dto = new ConfigDto(); dto.setPublishMode(postService.getPublishMode()); @@ -28,6 +37,10 @@ public class AdminConfigController { } @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) { if (dto.getPublishMode() != null) { postService.setPublishMode(dto.getPublishMode()); diff --git a/backend/src/main/java/com/openisle/controller/AdminController.java b/backend/src/main/java/com/openisle/controller/AdminController.java index 2084ea196..968800dbb 100644 --- a/backend/src/main/java/com/openisle/controller/AdminController.java +++ b/backend/src/main/java/com/openisle/controller/AdminController.java @@ -1,5 +1,10 @@ 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.RestController; import java.util.Map; @@ -10,6 +15,10 @@ import java.util.Map; @RestController public class AdminController { @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 adminHello() { return Map.of("message", "Hello, Admin User"); } diff --git a/backend/src/main/java/com/openisle/controller/AdminPostController.java b/backend/src/main/java/com/openisle/controller/AdminPostController.java index 498c05add..6848aad39 100644 --- a/backend/src/main/java/com/openisle/controller/AdminPostController.java +++ b/backend/src/main/java/com/openisle/controller/AdminPostController.java @@ -3,6 +3,12 @@ package com.openisle.controller; import com.openisle.dto.PostSummaryDto; import com.openisle.mapper.PostMapper; 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 org.springframework.web.bind.annotation.*; @@ -20,6 +26,10 @@ public class AdminPostController { private final PostMapper postMapper; @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 pendingPosts() { return postService.listPendingPosts().stream() .map(postMapper::toSummaryDto) @@ -27,31 +37,55 @@ public class AdminPostController { } @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) { return postMapper.toSummaryDto(postService.approvePost(id)); } @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) { return postMapper.toSummaryDto(postService.rejectPost(id)); } @PostMapping("/{id}/pin") + @SecurityRequirement(name = "JWT") + @Operation(summary = "Pin post", description = "Pin a post to the top") + @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") + @SecurityRequirement(name = "JWT") + @Operation(summary = "Unpin post", description = "Remove a post from the top") + @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") + @SecurityRequirement(name = "JWT") + @Operation(summary = "Exclude from RSS", description = "Exclude a post from RSS feed") + @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") + @SecurityRequirement(name = "JWT") + @Operation(summary = "Include in RSS", description = "Include a post in the RSS feed") + @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())); } diff --git a/backend/src/main/java/com/openisle/controller/AdminTagController.java b/backend/src/main/java/com/openisle/controller/AdminTagController.java index 3dba36f6c..d0cbf4f8e 100644 --- a/backend/src/main/java/com/openisle/controller/AdminTagController.java +++ b/backend/src/main/java/com/openisle/controller/AdminTagController.java @@ -5,6 +5,12 @@ import com.openisle.mapper.TagMapper; import com.openisle.model.Tag; import com.openisle.service.PostService; 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 org.springframework.web.bind.annotation.*; @@ -20,6 +26,10 @@ public class AdminTagController { private final TagMapper tagMapper; @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 pendingTags() { return tagService.listPendingTags().stream() .map(t -> tagMapper.toDto(t, postService.countPostsByTag(t.getId()))) @@ -27,6 +37,10 @@ public class AdminTagController { } @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) { Tag tag = tagService.approveTag(id); long count = postService.countPostsByTag(tag.getId()); diff --git a/backend/src/main/java/com/openisle/controller/AdminUserController.java b/backend/src/main/java/com/openisle/controller/AdminUserController.java index 4beed5ce5..b149d49ad 100644 --- a/backend/src/main/java/com/openisle/controller/AdminUserController.java +++ b/backend/src/main/java/com/openisle/controller/AdminUserController.java @@ -6,6 +6,9 @@ import com.openisle.model.User; import com.openisle.service.EmailSender; import com.openisle.repository.NotificationRepository; 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 org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; @@ -22,6 +25,9 @@ public class AdminUserController { private String websiteUrl; @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) { User user = userRepository.findById(id).orElseThrow(); user.setApproved(true); @@ -33,6 +39,9 @@ public class AdminUserController { } @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) { User user = userRepository.findById(id).orElseThrow(); user.setApproved(false); diff --git a/backend/src/main/java/com/openisle/controller/AiController.java b/backend/src/main/java/com/openisle/controller/AiController.java index 6e5dca1fd..ff259cf79 100644 --- a/backend/src/main/java/com/openisle/controller/AiController.java +++ b/backend/src/main/java/com/openisle/controller/AiController.java @@ -9,6 +9,11 @@ import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; 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; @@ -21,6 +26,10 @@ public class AiController { private final AiUsageService aiUsageService; @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> format(@RequestBody Map req, Authentication auth) { String text = req.get("text"); diff --git a/backend/src/main/java/com/openisle/controller/AuthController.java b/backend/src/main/java/com/openisle/controller/AuthController.java index 0056c3c9b..29e8a6348 100644 --- a/backend/src/main/java/com/openisle/controller/AuthController.java +++ b/backend/src/main/java/com/openisle/controller/AuthController.java @@ -8,6 +8,11 @@ import com.openisle.model.User; import com.openisle.repository.UserRepository; 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 org.springframework.beans.factory.annotation.Value; import org.springframework.data.redis.core.RedisTemplate; @@ -47,6 +52,9 @@ public class AuthController { private boolean loginCaptchaEnabled; @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) { if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha")); @@ -84,6 +92,9 @@ public class AuthController { } @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) { Optional userOpt = userService.findByUsername(req.getUsername()); if (userOpt.isEmpty()) { @@ -111,6 +122,9 @@ public class AuthController { } @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) { if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha")); @@ -149,6 +163,9 @@ public class AuthController { } @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) { boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); @@ -196,6 +213,9 @@ public class AuthController { @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) { String username = jwtService.validateAndGetSubjectForReason(req.getToken()); Optional userOpt = userService.findByUsername(username); @@ -224,6 +244,9 @@ public class AuthController { } @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) { boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); @@ -272,6 +295,9 @@ public class AuthController { } @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) { boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); @@ -319,6 +345,9 @@ public class AuthController { } @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) { boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); @@ -367,6 +396,9 @@ public class AuthController { } @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) { boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty(); InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken()); @@ -412,11 +444,18 @@ public class AuthController { } @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() { return ResponseEntity.ok(Map.of("valid", true)); } @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) { Optional userOpt = userService.findByEmail(req.getEmail()); if (userOpt.isEmpty()) { @@ -427,6 +466,9 @@ public class AuthController { } @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) { Optional userOpt = userService.findByEmail(req.getEmail()); if (userOpt.isEmpty()) { @@ -441,6 +483,9 @@ public class AuthController { } @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) { String username = jwtService.validateAndGetSubjectForReset(req.getToken()); try { diff --git a/backend/src/main/java/com/openisle/controller/CategoryController.java b/backend/src/main/java/com/openisle/controller/CategoryController.java index 37282bc39..02c8ed2bc 100644 --- a/backend/src/main/java/com/openisle/controller/CategoryController.java +++ b/backend/src/main/java/com/openisle/controller/CategoryController.java @@ -10,6 +10,11 @@ import com.openisle.service.CategoryService; import com.openisle.service.PostService; 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.Map; @@ -25,6 +30,9 @@ public class CategoryController { private final CategoryMapper categoryMapper; @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) { Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); long count = postService.countPostsByCategory(c.getId()); @@ -32,6 +40,9 @@ public class CategoryController { } @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) { Category c = categoryService.updateCategory(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); long count = postService.countPostsByCategory(c.getId()); @@ -39,11 +50,16 @@ public class CategoryController { } @DeleteMapping("/{id}") + @Operation(summary = "Delete category", description = "Remove a category by id") + @ApiResponse(responseCode = "200", description = "Category deleted") public void delete(@PathVariable Long id) { categoryService.deleteCategory(id); } @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 list() { List all = categoryService.listCategories(); List ids = all.stream().map(Category::getId).toList(); @@ -55,6 +71,9 @@ public class CategoryController { } @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) { Category c = categoryService.getCategory(id); long count = postService.countPostsByCategory(c.getId()); @@ -62,6 +81,9 @@ public class CategoryController { } @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 listPostsByCategory(@PathVariable Long id, @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "pageSize", required = false) Integer pageSize) { diff --git a/backend/src/main/java/com/openisle/controller/ChannelController.java b/backend/src/main/java/com/openisle/controller/ChannelController.java index 03b5a6952..41c680b43 100644 --- a/backend/src/main/java/com/openisle/controller/ChannelController.java +++ b/backend/src/main/java/com/openisle/controller/ChannelController.java @@ -8,6 +8,12 @@ import com.openisle.service.MessageService; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; 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; @@ -26,16 +32,28 @@ public class ChannelController { } @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 listChannels(Authentication auth) { return channelService.listChannels(getCurrentUserId(auth)); } @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) { return channelService.joinChannel(channelId, getCurrentUserId(auth)); } @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) { return messageService.getUnreadChannelCount(getCurrentUserId(auth)); } diff --git a/backend/src/main/java/com/openisle/controller/CommentController.java b/backend/src/main/java/com/openisle/controller/CommentController.java index 77b3b0b16..d611e9622 100644 --- a/backend/src/main/java/com/openisle/controller/CommentController.java +++ b/backend/src/main/java/com/openisle/controller/CommentController.java @@ -14,6 +14,12 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; 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.stream.Collectors; @@ -36,6 +42,10 @@ public class CommentController { private boolean commentCaptchaEnabled; @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 createComment(@PathVariable Long postId, @RequestBody CommentRequest req, Authentication auth) { @@ -53,6 +63,10 @@ public class CommentController { } @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 replyComment(@PathVariable Long commentId, @RequestBody CommentRequest req, Authentication auth) { @@ -69,6 +83,9 @@ public class CommentController { } @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 listComments(@PathVariable Long postId, @RequestParam(value = "sort", required = false, defaultValue = "OLDEST") com.openisle.model.CommentSort sort) { log.debug("listComments called for post {} with sort {}", postId, sort); @@ -80,6 +97,9 @@ public class CommentController { } @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) { log.debug("deleteComment called by user {} for comment {}", auth.getName(), id); commentService.deleteComment(auth.getName(), id); @@ -87,12 +107,20 @@ public class CommentController { } @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) { log.debug("pinComment called by user {} for comment {}", auth.getName(), id); return commentMapper.toDto(commentService.pinComment(auth.getName(), id)); } @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) { log.debug("unpinComment called by user {} for comment {}", auth.getName(), id); return commentMapper.toDto(commentService.unpinComment(auth.getName(), id)); diff --git a/backend/src/main/java/com/openisle/controller/ConfigController.java b/backend/src/main/java/com/openisle/controller/ConfigController.java index b4c115978..8139ebc58 100644 --- a/backend/src/main/java/com/openisle/controller/ConfigController.java +++ b/backend/src/main/java/com/openisle/controller/ConfigController.java @@ -6,6 +6,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; 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 @RequestMapping("/api") @@ -33,6 +37,9 @@ public class ConfigController { private final RegisterModeService registerModeService; @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() { SiteConfigDto resp = new SiteConfigDto(); resp.setCaptchaEnabled(captchaEnabled); diff --git a/backend/src/main/java/com/openisle/controller/DraftController.java b/backend/src/main/java/com/openisle/controller/DraftController.java index 4f9ecd408..f0e481787 100644 --- a/backend/src/main/java/com/openisle/controller/DraftController.java +++ b/backend/src/main/java/com/openisle/controller/DraftController.java @@ -9,6 +9,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; 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 @RequestMapping("/api/drafts") @@ -18,12 +23,20 @@ public class DraftController { private final DraftMapper draftMapper; @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 saveDraft(@RequestBody DraftRequest req, Authentication auth) { Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds()); return ResponseEntity.ok(draftMapper.toDto(draft)); } @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 getMyDraft(Authentication auth) { return draftService.getDraft(auth.getName()) .map(d -> ResponseEntity.ok(draftMapper.toDto(d))) @@ -31,6 +44,9 @@ public class DraftController { } @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) { draftService.deleteDraft(auth.getName()); return ResponseEntity.ok().build(); diff --git a/backend/src/main/java/com/openisle/controller/HelloController.java b/backend/src/main/java/com/openisle/controller/HelloController.java index de9baf7e3..e77d48876 100644 --- a/backend/src/main/java/com/openisle/controller/HelloController.java +++ b/backend/src/main/java/com/openisle/controller/HelloController.java @@ -1,5 +1,10 @@ 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.RestController; import java.util.Map; @@ -7,6 +12,10 @@ import java.util.Map; @RestController public class HelloController { @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 hello() { return Map.of("message", "Hello, Authenticated User"); } diff --git a/backend/src/main/java/com/openisle/controller/InviteController.java b/backend/src/main/java/com/openisle/controller/InviteController.java index 9c0817dc9..6d6e93c89 100644 --- a/backend/src/main/java/com/openisle/controller/InviteController.java +++ b/backend/src/main/java/com/openisle/controller/InviteController.java @@ -6,6 +6,11 @@ import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestMapping; 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; @@ -16,6 +21,10 @@ public class InviteController { private final InviteService inviteService; @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 generate(Authentication auth) { String token = inviteService.generate(auth.getName()); return Map.of("token", token); diff --git a/backend/src/main/java/com/openisle/controller/MedalController.java b/backend/src/main/java/com/openisle/controller/MedalController.java index b710f7557..dfe113239 100644 --- a/backend/src/main/java/com/openisle/controller/MedalController.java +++ b/backend/src/main/java/com/openisle/controller/MedalController.java @@ -7,6 +7,12 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; 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; @@ -17,11 +23,17 @@ public class MedalController { private final MedalService medalService; @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 getMedals(@RequestParam(value = "userId", required = false) Long userId) { return medalService.getMedals(userId); } @PostMapping("/select") + @Operation(summary = "Select medal", description = "Select a medal for current user") + @ApiResponse(responseCode = "200", description = "Medal selected") + @SecurityRequirement(name = "JWT") public ResponseEntity selectMedal(@RequestBody MedalSelectRequest req, Authentication auth) { try { medalService.selectMedal(auth.getName(), req.getType()); diff --git a/backend/src/main/java/com/openisle/controller/MessageController.java b/backend/src/main/java/com/openisle/controller/MessageController.java index 500ad2a05..8173b59ed 100644 --- a/backend/src/main/java/com/openisle/controller/MessageController.java +++ b/backend/src/main/java/com/openisle/controller/MessageController.java @@ -18,6 +18,12 @@ import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; 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; @@ -37,12 +43,20 @@ public class MessageController { } @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> getConversations(Authentication auth) { List conversations = messageService.getConversations(getCurrentUserId(auth)); return ResponseEntity.ok(conversations); } @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 getMessages(@PathVariable Long conversationId, @RequestParam(defaultValue = "0") int page, @RequestParam(defaultValue = "20") int size, @@ -53,12 +67,20 @@ public class MessageController { } @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 sendMessage(@RequestBody MessageRequest req, Authentication auth) { Message message = messageService.sendMessage(getCurrentUserId(auth), req.getRecipientId(), req.getContent(), req.getReplyToId()); return ResponseEntity.ok(messageService.toDto(message)); } @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 sendMessageToConversation(@PathVariable Long conversationId, @RequestBody ChannelMessageRequest req, Authentication auth) { @@ -67,18 +89,29 @@ public class MessageController { } @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 markAsRead(@PathVariable Long conversationId, Authentication auth) { messageService.markConversationAsRead(conversationId, getCurrentUserId(auth)); return ResponseEntity.ok().build(); } @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 findOrCreateConversation(@RequestBody CreateConversationRequest req, Authentication auth) { MessageConversation conversation = messageService.findOrCreateConversation(getCurrentUserId(auth), req.getRecipientId()); return ResponseEntity.ok(new CreateConversationResponse(conversation.getId())); } @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 getUnreadCount(Authentication auth) { return ResponseEntity.ok(messageService.getUnreadMessageCount(getCurrentUserId(auth))); } diff --git a/backend/src/main/java/com/openisle/controller/NotificationController.java b/backend/src/main/java/com/openisle/controller/NotificationController.java index 03fe52e7e..3371fcda6 100644 --- a/backend/src/main/java/com/openisle/controller/NotificationController.java +++ b/backend/src/main/java/com/openisle/controller/NotificationController.java @@ -10,6 +10,12 @@ import com.openisle.service.NotificationService; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; 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.stream.Collectors; @@ -23,6 +29,10 @@ public class NotificationController { private final NotificationMapper notificationMapper; @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 list(@RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "30") int size, Authentication auth) { @@ -32,6 +42,10 @@ public class NotificationController { } @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 listUnread(@RequestParam(value = "page", defaultValue = "0") int page, @RequestParam(value = "size", defaultValue = "30") int size, Authentication auth) { @@ -41,6 +55,10 @@ public class NotificationController { } @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) { long count = notificationService.countUnread(auth.getName()); NotificationUnreadCountDto uc = new NotificationUnreadCountDto(); @@ -49,26 +67,43 @@ public class NotificationController { } @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) { notificationService.markRead(auth.getName(), req.getIds()); } @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 prefs(Authentication auth) { return notificationService.listPreferences(auth.getName()); } @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) { notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled()); } @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 emailPrefs(Authentication auth) { return notificationService.listEmailPreferences(auth.getName()); } @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) { notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled()); } diff --git a/backend/src/main/java/com/openisle/controller/OnlineController.java b/backend/src/main/java/com/openisle/controller/OnlineController.java index 2f6a307c2..3119287f0 100644 --- a/backend/src/main/java/com/openisle/controller/OnlineController.java +++ b/backend/src/main/java/com/openisle/controller/OnlineController.java @@ -5,6 +5,10 @@ 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; @@ -22,11 +26,16 @@ public class OnlineController { 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(); } diff --git a/backend/src/main/java/com/openisle/controller/PointHistoryController.java b/backend/src/main/java/com/openisle/controller/PointHistoryController.java index 1a4235e3a..0d8dc60bb 100644 --- a/backend/src/main/java/com/openisle/controller/PointHistoryController.java +++ b/backend/src/main/java/com/openisle/controller/PointHistoryController.java @@ -9,6 +9,12 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; 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.Map; @@ -22,6 +28,10 @@ public class PointHistoryController { private final PointHistoryMapper pointHistoryMapper; @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 list(Authentication auth) { return pointService.listHistory(auth.getName()).stream() .map(pointHistoryMapper::toDto) @@ -29,6 +39,10 @@ public class PointHistoryController { } @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> trend(Authentication auth, @RequestParam(value = "days", defaultValue = "30") int days) { return pointService.trend(auth.getName(), days); diff --git a/backend/src/main/java/com/openisle/controller/PointMallController.java b/backend/src/main/java/com/openisle/controller/PointMallController.java index eb6066f52..b9cf9f916 100644 --- a/backend/src/main/java/com/openisle/controller/PointMallController.java +++ b/backend/src/main/java/com/openisle/controller/PointMallController.java @@ -9,6 +9,12 @@ import com.openisle.service.UserService; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; 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.Map; @@ -24,6 +30,9 @@ public class PointMallController { private final PointGoodMapper pointGoodMapper; @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 list() { return pointMallService.listGoods().stream() .map(pointGoodMapper::toDto) @@ -31,6 +40,10 @@ public class PointMallController { } @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 redeem(@RequestBody PointRedeemRequest req, Authentication auth) { User user = userService.findByIdentifier(auth.getName()).orElseThrow(); int point = pointMallService.redeem(user, req.getGoodId(), req.getContact()); diff --git a/backend/src/main/java/com/openisle/controller/PostChangeLogController.java b/backend/src/main/java/com/openisle/controller/PostChangeLogController.java index ae51a9dbb..f7f555d2d 100644 --- a/backend/src/main/java/com/openisle/controller/PostChangeLogController.java +++ b/backend/src/main/java/com/openisle/controller/PostChangeLogController.java @@ -5,6 +5,11 @@ 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; @@ -17,6 +22,9 @@ public class PostChangeLogController { 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 listLogs(@PathVariable Long id) { return changeLogService.listLogs(id).stream() .map(mapper::toDto) diff --git a/backend/src/main/java/com/openisle/controller/PostController.java b/backend/src/main/java/com/openisle/controller/PostController.java index 598bc7ed2..af37be3e4 100644 --- a/backend/src/main/java/com/openisle/controller/PostController.java +++ b/backend/src/main/java/com/openisle/controller/PostController.java @@ -7,6 +7,12 @@ import com.openisle.dto.PollDto; import com.openisle.mapper.PostMapper; import com.openisle.model.Post; 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 org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; @@ -35,6 +41,10 @@ public class PostController { private boolean postCaptchaEnabled; @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 createPost(@RequestBody PostRequest req, Authentication auth) { if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { return ResponseEntity.badRequest().build(); @@ -53,6 +63,10 @@ public class PostController { } @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 updatePost(@PathVariable Long id, @RequestBody PostRequest req, Authentication auth) { Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(), @@ -61,21 +75,35 @@ public class PostController { } @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) { postService.deletePost(id, auth.getName()); } @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) { return postMapper.toSummaryDto(postService.closePost(id, auth.getName())); } @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) { return postMapper.toSummaryDto(postService.reopenPost(id, auth.getName())); } @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 getPost(@PathVariable Long id, Authentication auth) { String viewer = auth != null ? auth.getName() : null; Post post = postService.viewPost(id, viewer); @@ -83,23 +111,35 @@ public class PostController { } @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 joinLottery(@PathVariable Long id, Authentication auth) { postService.joinLottery(id, auth.getName()); return ResponseEntity.ok().build(); } @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 pollProgress(@PathVariable Long id) { return ResponseEntity.ok(postMapper.toSummaryDto(postService.getPoll(id)).getPoll()); } @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 vote(@PathVariable Long id, @RequestParam("option") List option, Authentication auth) { postService.votePoll(id, auth.getName(), option); return ResponseEntity.ok().build(); } @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 listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, @RequestParam(value = "categoryIds", required = false) List categoryIds, @RequestParam(value = "tagId", required = false) Long tagId, @@ -137,6 +177,9 @@ public class PostController { } @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 rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, @RequestParam(value = "categoryIds", required = false) List categoryIds, @RequestParam(value = "tagId", required = false) Long tagId, @@ -162,6 +205,9 @@ public class PostController { } @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 latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, @RequestParam(value = "categoryIds", required = false) List categoryIds, @RequestParam(value = "tagId", required = false) Long tagId, @@ -187,6 +233,9 @@ public class PostController { } @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 featuredPosts(@RequestParam(value = "categoryId", required = false) Long categoryId, @RequestParam(value = "categoryIds", required = false) List categoryIds, @RequestParam(value = "tagId", required = false) Long tagId, diff --git a/backend/src/main/java/com/openisle/controller/PushSubscriptionController.java b/backend/src/main/java/com/openisle/controller/PushSubscriptionController.java index 48450592a..3c9546b99 100644 --- a/backend/src/main/java/com/openisle/controller/PushSubscriptionController.java +++ b/backend/src/main/java/com/openisle/controller/PushSubscriptionController.java @@ -7,6 +7,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.security.core.Authentication; 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 @RequestMapping("/api/push") @@ -17,6 +22,9 @@ public class PushSubscriptionController { private String publicKey; @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() { PushPublicKeyDto r = new PushPublicKeyDto(); r.setKey(publicKey); @@ -24,6 +32,9 @@ public class PushSubscriptionController { } @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) { pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth()); } diff --git a/backend/src/main/java/com/openisle/controller/ReactionController.java b/backend/src/main/java/com/openisle/controller/ReactionController.java index 47450834a..93064cb27 100644 --- a/backend/src/main/java/com/openisle/controller/ReactionController.java +++ b/backend/src/main/java/com/openisle/controller/ReactionController.java @@ -12,6 +12,11 @@ import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; 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 @RequestMapping("/api") @@ -26,11 +31,18 @@ public class ReactionController { * Get all available 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() { return ReactionType.values(); } @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 reactToPost(@PathVariable Long postId, @RequestBody ReactionRequest req, Authentication auth) { @@ -46,6 +58,10 @@ public class ReactionController { } @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 reactToComment(@PathVariable Long commentId, @RequestBody ReactionRequest req, Authentication auth) { @@ -61,6 +77,10 @@ public class ReactionController { } @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 reactToMessage(@PathVariable Long messageId, @RequestBody ReactionRequest req, Authentication auth) { diff --git a/backend/src/main/java/com/openisle/controller/RssController.java b/backend/src/main/java/com/openisle/controller/RssController.java index 6a3bddfb9..ba402afcf 100644 --- a/backend/src/main/java/com/openisle/controller/RssController.java +++ b/backend/src/main/java/com/openisle/controller/RssController.java @@ -13,6 +13,10 @@ import org.jsoup.safety.Safelist; import org.springframework.beans.factory.annotation.Value; import org.springframework.web.bind.annotation.GetMapping; 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.tables.TablesExtension; @@ -63,6 +67,8 @@ public class RssController { } @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() { // 建议 20;你现在是 10,这里保留你的 10 List posts = postService.listLatestRssPosts(10); diff --git a/backend/src/main/java/com/openisle/controller/SearchController.java b/backend/src/main/java/com/openisle/controller/SearchController.java index 0380ee41b..034d293e8 100644 --- a/backend/src/main/java/com/openisle/controller/SearchController.java +++ b/backend/src/main/java/com/openisle/controller/SearchController.java @@ -11,6 +11,11 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; 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.stream.Collectors; @@ -24,6 +29,9 @@ public class SearchController { private final PostMapper postMapper; @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 searchUsers(@RequestParam String keyword) { return searchService.searchUsers(keyword).stream() .map(userMapper::toDto) @@ -31,6 +39,9 @@ public class SearchController { } @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 searchPosts(@RequestParam String keyword) { return searchService.searchPosts(keyword).stream() .map(postMapper::toSummaryDto) @@ -38,6 +49,9 @@ public class SearchController { } @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 searchPostsByContent(@RequestParam String keyword) { return searchService.searchPostsByContent(keyword).stream() .map(postMapper::toSummaryDto) @@ -45,6 +59,9 @@ public class SearchController { } @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 searchPostsByTitle(@RequestParam String keyword) { return searchService.searchPostsByTitle(keyword).stream() .map(postMapper::toSummaryDto) @@ -52,6 +69,9 @@ public class SearchController { } @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 global(@RequestParam String keyword) { return searchService.globalSearch(keyword).stream() .map(r -> { diff --git a/backend/src/main/java/com/openisle/controller/SitemapController.java b/backend/src/main/java/com/openisle/controller/SitemapController.java index e7e03f40a..a7c56ab8a 100644 --- a/backend/src/main/java/com/openisle/controller/SitemapController.java +++ b/backend/src/main/java/com/openisle/controller/SitemapController.java @@ -10,6 +10,10 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; 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; @@ -26,6 +30,9 @@ public class SitemapController { private String websiteUrl; @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 sitemap() { List posts = postRepository.findByStatus(PostStatus.PUBLISHED); diff --git a/backend/src/main/java/com/openisle/controller/StatController.java b/backend/src/main/java/com/openisle/controller/StatController.java index 80701c3ec..2bcf9a1a8 100644 --- a/backend/src/main/java/com/openisle/controller/StatController.java +++ b/backend/src/main/java/com/openisle/controller/StatController.java @@ -8,6 +8,11 @@ import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; 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.util.List; @@ -21,6 +26,9 @@ public class StatController { private final StatService statService; @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 dau(@RequestParam(value = "date", required = false) @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) { long count = userVisitService.countDau(date); @@ -28,6 +36,9 @@ public class StatController { } @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> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) { if (days < 1) days = 1; LocalDate end = LocalDate.now(); @@ -42,6 +53,9 @@ public class StatController { } @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> newUsersRange(@RequestParam(value = "days", defaultValue = "30") int days) { if (days < 1) days = 1; LocalDate end = LocalDate.now(); @@ -56,6 +70,9 @@ public class StatController { } @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> postsRange(@RequestParam(value = "days", defaultValue = "30") int days) { if (days < 1) days = 1; LocalDate end = LocalDate.now(); @@ -70,6 +87,9 @@ public class StatController { } @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> commentsRange(@RequestParam(value = "days", defaultValue = "30") int days) { if (days < 1) days = 1; LocalDate end = LocalDate.now(); diff --git a/backend/src/main/java/com/openisle/controller/SubscriptionController.java b/backend/src/main/java/com/openisle/controller/SubscriptionController.java index 1ed486c26..3c5fe351d 100644 --- a/backend/src/main/java/com/openisle/controller/SubscriptionController.java +++ b/backend/src/main/java/com/openisle/controller/SubscriptionController.java @@ -4,6 +4,9 @@ import com.openisle.service.SubscriptionService; import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; 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. */ @RestController @@ -13,31 +16,49 @@ public class SubscriptionController { private final SubscriptionService subscriptionService; @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) { subscriptionService.subscribePost(auth.getName(), 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) { subscriptionService.unsubscribePost(auth.getName(), postId); } @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) { subscriptionService.subscribeComment(auth.getName(), 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) { subscriptionService.unsubscribeComment(auth.getName(), commentId); } @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) { subscriptionService.subscribeUser(auth.getName(), 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) { subscriptionService.unsubscribeUser(auth.getName(), username); } diff --git a/backend/src/main/java/com/openisle/controller/TagController.java b/backend/src/main/java/com/openisle/controller/TagController.java index 3c7df2421..b5a388849 100644 --- a/backend/src/main/java/com/openisle/controller/TagController.java +++ b/backend/src/main/java/com/openisle/controller/TagController.java @@ -13,6 +13,12 @@ import com.openisle.service.PostService; import com.openisle.service.TagService; 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 io.swagger.v3.oas.annotations.security.SecurityRequirement; import java.util.List; import java.util.Map; @@ -29,6 +35,10 @@ public class TagController { private final TagMapper tagMapper; @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) { boolean approved = true; if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) { @@ -49,6 +59,9 @@ public class TagController { } @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) { Tag tag = tagService.updateTag(id, req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon()); long count = postService.countPostsByTag(tag.getId()); @@ -56,11 +69,16 @@ public class TagController { } @DeleteMapping("/{id}") + @Operation(summary = "Delete tag", description = "Delete a tag by id") + @ApiResponse(responseCode = "200", description = "Tag deleted") public void delete(@PathVariable Long id) { tagService.deleteTag(id); } @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 list(@RequestParam(value = "keyword", required = false) String keyword, @RequestParam(value = "limit", required = false) Integer limit) { List tags = tagService.searchTags(keyword); @@ -77,6 +95,9 @@ public class TagController { } @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) { Tag tag = tagService.getTag(id); long count = postService.countPostsByTag(tag.getId()); @@ -84,6 +105,9 @@ public class TagController { } @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 listPostsByTag(@PathVariable Long id, @RequestParam(value = "page", required = false) Integer page, @RequestParam(value = "pageSize", required = false) Integer pageSize) { diff --git a/backend/src/main/java/com/openisle/controller/UploadController.java b/backend/src/main/java/com/openisle/controller/UploadController.java index fe27b2917..fb2186596 100644 --- a/backend/src/main/java/com/openisle/controller/UploadController.java +++ b/backend/src/main/java/com/openisle/controller/UploadController.java @@ -6,6 +6,10 @@ import org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; 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.IOException; @@ -27,6 +31,9 @@ public class UploadController { private long maxUploadSize; @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) { if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) { return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image")); @@ -48,6 +55,9 @@ public class UploadController { } @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 body) { String link = body.get("url"); if (link == null || link.isBlank()) { @@ -76,6 +86,9 @@ public class UploadController { } @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 presign(@RequestParam("filename") String filename) { return imageUploader.presignUpload(filename); } diff --git a/backend/src/main/java/com/openisle/controller/UserController.java b/backend/src/main/java/com/openisle/controller/UserController.java index c3c737ce4..8c26d9f40 100644 --- a/backend/src/main/java/com/openisle/controller/UserController.java +++ b/backend/src/main/java/com/openisle/controller/UserController.java @@ -6,6 +6,12 @@ import com.openisle.mapper.TagMapper; import com.openisle.mapper.UserMapper; import com.openisle.model.User; 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 org.springframework.beans.factory.annotation.Value; import org.springframework.http.ResponseEntity; @@ -48,12 +54,20 @@ public class UserController { private int defaultTagsLimit; @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 me(Authentication auth) { User user = userService.findByUsername(auth.getName()).orElseThrow(); return ResponseEntity.ok(userMapper.toDto(user, auth)); } @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, Authentication auth) { if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) { @@ -73,6 +87,10 @@ public class UserController { } @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, Authentication auth) { User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction()); @@ -83,12 +101,19 @@ public class UserController { } @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 signIn(Authentication auth) { int reward = levelService.awardForSignin(auth.getName()); return Map.of("reward", reward); } @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 getUser(@PathVariable("identifier") String identifier, Authentication auth) { User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found")); @@ -96,6 +121,9 @@ public class UserController { } @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 userPosts(@PathVariable("identifier") String identifier, @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : defaultPostsLimit; @@ -106,6 +134,9 @@ public class UserController { } @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 subscribedPosts(@PathVariable("identifier") String identifier, @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : defaultPostsLimit; @@ -117,6 +148,9 @@ public class UserController { } @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 userReplies(@PathVariable("identifier") String identifier, @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : defaultRepliesLimit; @@ -127,6 +161,9 @@ public class UserController { } @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 hotPosts(@PathVariable("identifier") String identifier, @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : 10; @@ -138,6 +175,9 @@ public class UserController { } @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 hotReplies(@PathVariable("identifier") String identifier, @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : 10; @@ -149,6 +189,9 @@ public class UserController { } @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 hotTags(@PathVariable("identifier") String identifier, @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : 10; @@ -161,6 +204,9 @@ public class UserController { } @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 userTags(@PathVariable("identifier") String identifier, @RequestParam(value = "limit", required = false) Integer limit) { int l = limit != null ? limit : defaultTagsLimit; @@ -171,6 +217,9 @@ public class UserController { } @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 following(@PathVariable("identifier") String identifier) { User user = userService.findByIdentifier(identifier).orElseThrow(); return subscriptionService.getSubscribedUsers(user.getUsername()).stream() @@ -179,6 +228,9 @@ public class UserController { } @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 followers(@PathVariable("identifier") String identifier) { User user = userService.findByIdentifier(identifier).orElseThrow(); return subscriptionService.getSubscribers(user.getUsername()).stream() @@ -187,6 +239,9 @@ public class UserController { } @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 admins() { return userService.getAdmins().stream() .map(userMapper::toDto) @@ -194,6 +249,9 @@ public class UserController { } @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 userAggregate(@PathVariable("identifier") String identifier, @RequestParam(value = "postsLimit", required = false) Integer postsLimit, @RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,