mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-24 15:10:48 +08:00
优化目录结构
This commit is contained in:
@@ -0,0 +1,61 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Activity;
|
||||
import com.openisle.model.ActivityType;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.service.ActivityService;
|
||||
import com.openisle.service.UserService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/activities")
|
||||
@RequiredArgsConstructor
|
||||
public class ActivityController {
|
||||
private final ActivityService activityService;
|
||||
private final UserService userService;
|
||||
|
||||
@GetMapping
|
||||
public List<Activity> list() {
|
||||
return activityService.list();
|
||||
}
|
||||
|
||||
@GetMapping("/milk-tea")
|
||||
public MilkTeaInfo milkTea() {
|
||||
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
||||
long count = activityService.countParticipants(a);
|
||||
if (!a.isEnded() && count >= 50) {
|
||||
activityService.end(a);
|
||||
}
|
||||
MilkTeaInfo info = new MilkTeaInfo();
|
||||
info.setRedeemCount(count);
|
||||
info.setEnded(a.isEnded());
|
||||
return info;
|
||||
}
|
||||
|
||||
@PostMapping("/milk-tea/redeem")
|
||||
public java.util.Map<String, String> redeemMilkTea(@RequestBody RedeemRequest req, Authentication auth) {
|
||||
User user = userService.findByIdentifier(auth.getName()).orElseThrow();
|
||||
Activity a = activityService.getByType(ActivityType.MILK_TEA);
|
||||
boolean first = activityService.redeem(a, user, req.getContact());
|
||||
if (first) {
|
||||
return java.util.Map.of("message", "redeemed");
|
||||
}
|
||||
return java.util.Map.of("message", "updated");
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class MilkTeaInfo {
|
||||
private long redeemCount;
|
||||
private boolean ended;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class RedeemRequest {
|
||||
private String contact;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.PasswordStrength;
|
||||
import com.openisle.model.PublishMode;
|
||||
import com.openisle.service.PasswordValidator;
|
||||
import com.openisle.service.PostService;
|
||||
import com.openisle.service.AiUsageService;
|
||||
import com.openisle.service.RegisterModeService;
|
||||
import com.openisle.model.RegisterMode;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/config")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminConfigController {
|
||||
private final PostService postService;
|
||||
private final PasswordValidator passwordValidator;
|
||||
private final AiUsageService aiUsageService;
|
||||
private final RegisterModeService registerModeService;
|
||||
|
||||
@GetMapping
|
||||
public ConfigDto getConfig() {
|
||||
ConfigDto dto = new ConfigDto();
|
||||
dto.setPublishMode(postService.getPublishMode());
|
||||
dto.setPasswordStrength(passwordValidator.getStrength());
|
||||
dto.setAiFormatLimit(aiUsageService.getFormatLimit());
|
||||
dto.setRegisterMode(registerModeService.getRegisterMode());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@PostMapping
|
||||
public ConfigDto updateConfig(@RequestBody ConfigDto dto) {
|
||||
if (dto.getPublishMode() != null) {
|
||||
postService.setPublishMode(dto.getPublishMode());
|
||||
}
|
||||
if (dto.getPasswordStrength() != null) {
|
||||
passwordValidator.setStrength(dto.getPasswordStrength());
|
||||
}
|
||||
if (dto.getAiFormatLimit() != null) {
|
||||
aiUsageService.setFormatLimit(dto.getAiFormatLimit());
|
||||
}
|
||||
if (dto.getRegisterMode() != null) {
|
||||
registerModeService.setRegisterMode(dto.getRegisterMode());
|
||||
}
|
||||
return getConfig();
|
||||
}
|
||||
|
||||
@Data
|
||||
public static class ConfigDto {
|
||||
private PublishMode publishMode;
|
||||
private PasswordStrength passwordStrength;
|
||||
private Integer aiFormatLimit;
|
||||
private RegisterMode registerMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Simple admin demo endpoint.
|
||||
*/
|
||||
@RestController
|
||||
public class AdminController {
|
||||
@GetMapping("/api/admin/hello")
|
||||
public Map<String, String> adminHello() {
|
||||
return Map.of("message", "Hello, Admin User");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.service.PostService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* Endpoints for administrators to manage posts.
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/posts")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminPostController {
|
||||
private final PostService postService;
|
||||
|
||||
@GetMapping("/pending")
|
||||
public List<PostDto> pendingPosts() {
|
||||
return postService.listPendingPosts().stream()
|
||||
.map(this::toDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
public PostDto approve(@PathVariable Long id) {
|
||||
return toDto(postService.approvePost(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reject")
|
||||
public PostDto reject(@PathVariable Long id) {
|
||||
return toDto(postService.rejectPost(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/pin")
|
||||
public PostDto pin(@PathVariable Long id) {
|
||||
return toDto(postService.pinPost(id));
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/unpin")
|
||||
public PostDto unpin(@PathVariable Long id) {
|
||||
return toDto(postService.unpinPost(id));
|
||||
}
|
||||
|
||||
private PostDto toDto(Post post) {
|
||||
PostDto dto = new PostDto();
|
||||
dto.setId(post.getId());
|
||||
dto.setTitle(post.getTitle());
|
||||
dto.setContent(post.getContent());
|
||||
dto.setCreatedAt(post.getCreatedAt());
|
||||
dto.setAuthor(post.getAuthor().getUsername());
|
||||
dto.setCategory(toCategoryDto(post.getCategory()));
|
||||
dto.setViews(post.getViews());
|
||||
dto.setStatus(post.getStatus());
|
||||
dto.setPinnedAt(post.getPinnedAt());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private CategoryDto toCategoryDto(com.openisle.model.Category c) {
|
||||
CategoryDto dto = new CategoryDto();
|
||||
dto.setId(c.getId());
|
||||
dto.setName(c.getName());
|
||||
dto.setDescription(c.getDescription());
|
||||
dto.setIcon(c.getIcon());
|
||||
dto.setSmallIcon(c.getSmallIcon());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PostDto {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private String author;
|
||||
private CategoryDto category;
|
||||
private long views;
|
||||
private com.openisle.model.PostStatus status;
|
||||
private LocalDateTime pinnedAt;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CategoryDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Tag;
|
||||
import com.openisle.service.TagService;
|
||||
import com.openisle.service.PostService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/tags")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminTagController {
|
||||
private final TagService tagService;
|
||||
private final PostService postService;
|
||||
|
||||
@GetMapping("/pending")
|
||||
public List<TagDto> pendingTags() {
|
||||
return tagService.listPendingTags().stream()
|
||||
.map(t -> toDto(t, postService.countPostsByTag(t.getId())))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
public TagDto approve(@PathVariable Long id) {
|
||||
Tag tag = tagService.approveTag(id);
|
||||
long count = postService.countPostsByTag(tag.getId());
|
||||
return toDto(tag, count);
|
||||
}
|
||||
|
||||
private TagDto toDto(Tag tag, long count) {
|
||||
TagDto dto = new TagDto();
|
||||
dto.setId(tag.getId());
|
||||
dto.setName(tag.getName());
|
||||
dto.setDescription(tag.getDescription());
|
||||
dto.setIcon(tag.getIcon());
|
||||
dto.setSmallIcon(tag.getSmallIcon());
|
||||
dto.setCount(count);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TagDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
private Long count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.service.EmailSender;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/admin/users")
|
||||
@RequiredArgsConstructor
|
||||
public class AdminUserController {
|
||||
private final UserRepository userRepository;
|
||||
private final EmailSender emailSender;
|
||||
@Value("${app.website-url}")
|
||||
private String websiteUrl;
|
||||
|
||||
@PostMapping("/{id}/approve")
|
||||
public ResponseEntity<?> approve(@PathVariable Long id) {
|
||||
User user = userRepository.findById(id).orElseThrow();
|
||||
user.setApproved(true);
|
||||
userRepository.save(user);
|
||||
emailSender.sendEmail(user.getEmail(), "您的注册已审核通过",
|
||||
"🎉您的注册已经审核通过, 点击以访问网站: " + websiteUrl);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@PostMapping("/{id}/reject")
|
||||
public ResponseEntity<?> reject(@PathVariable Long id) {
|
||||
User user = userRepository.findById(id).orElseThrow();
|
||||
user.setApproved(false);
|
||||
userRepository.save(user);
|
||||
emailSender.sendEmail(user.getEmail(), "您的注册已被管理员拒绝",
|
||||
"您的注册被管理员拒绝, 点击链接可以重新填写理由申请: " + websiteUrl);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.service.OpenAiService;
|
||||
import com.openisle.service.AiUsageService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
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 java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/ai")
|
||||
@RequiredArgsConstructor
|
||||
public class AiController {
|
||||
|
||||
private final OpenAiService openAiService;
|
||||
private final AiUsageService aiUsageService;
|
||||
|
||||
@PostMapping("/format")
|
||||
public ResponseEntity<Map<String, String>> format(@RequestBody Map<String, String> req,
|
||||
Authentication auth) {
|
||||
String text = req.get("text");
|
||||
if (text == null) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
int limit = aiUsageService.getFormatLimit();
|
||||
int used = aiUsageService.getCount(auth.getName());
|
||||
if (limit > 0 && used >= limit) {
|
||||
return ResponseEntity.status(429).build();
|
||||
}
|
||||
aiUsageService.incrementAndGetCount(auth.getName());
|
||||
return openAiService.formatMarkdown(text)
|
||||
.map(t -> ResponseEntity.ok(Map.of("content", t)))
|
||||
.orElse(ResponseEntity.status(500).build());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,377 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.service.EmailSender;
|
||||
import com.openisle.service.JwtService;
|
||||
import com.openisle.service.UserService;
|
||||
import com.openisle.service.CaptchaService;
|
||||
import com.openisle.service.GoogleAuthService;
|
||||
import com.openisle.service.GithubAuthService;
|
||||
import com.openisle.service.DiscordAuthService;
|
||||
import com.openisle.service.TwitterAuthService;
|
||||
import com.openisle.service.RegisterModeService;
|
||||
import com.openisle.service.NotificationService;
|
||||
import com.openisle.model.RegisterMode;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.exception.FieldException;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/auth")
|
||||
@RequiredArgsConstructor
|
||||
public class AuthController {
|
||||
private final UserService userService;
|
||||
private final JwtService jwtService;
|
||||
private final EmailSender emailService;
|
||||
private final CaptchaService captchaService;
|
||||
private final GoogleAuthService googleAuthService;
|
||||
private final GithubAuthService githubAuthService;
|
||||
private final DiscordAuthService discordAuthService;
|
||||
private final TwitterAuthService twitterAuthService;
|
||||
private final RegisterModeService registerModeService;
|
||||
private final NotificationService notificationService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
|
||||
@Value("${app.captcha.enabled:false}")
|
||||
private boolean captchaEnabled;
|
||||
|
||||
@Value("${app.captcha.register-enabled:false}")
|
||||
private boolean registerCaptchaEnabled;
|
||||
|
||||
@Value("${app.captcha.login-enabled:false}")
|
||||
private boolean loginCaptchaEnabled;
|
||||
|
||||
@PostMapping("/register")
|
||||
public ResponseEntity<?> register(@RequestBody RegisterRequest req) {
|
||||
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
||||
}
|
||||
User user = userService.register(
|
||||
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
|
||||
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||
if (!user.isApproved()) {
|
||||
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
|
||||
}
|
||||
return ResponseEntity.ok(Map.of("message", "Verification code sent"));
|
||||
}
|
||||
|
||||
@PostMapping("/verify")
|
||||
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
||||
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
|
||||
if (ok) {
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"message", "Verified",
|
||||
"token", jwtService.generateReasonToken(req.getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
||||
}
|
||||
|
||||
@PostMapping("/login")
|
||||
public ResponseEntity<?> login(@RequestBody LoginRequest req) {
|
||||
if (captchaEnabled && loginCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
|
||||
}
|
||||
Optional<User> userOpt = userService.findByUsername(req.getUsername());
|
||||
if (userOpt.isEmpty()) {
|
||||
userOpt = userService.findByEmail(req.getUsername());
|
||||
}
|
||||
if (userOpt.isEmpty() || !userService.matchesPassword(userOpt.get(), req.getPassword())) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Invalid credentials",
|
||||
"reason_code", "INVALID_CREDENTIALS"));
|
||||
}
|
||||
User user = userOpt.get();
|
||||
if (!user.isVerified()) {
|
||||
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
|
||||
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "User not verified",
|
||||
"reason_code", "NOT_VERIFIED",
|
||||
"user_name", user.getUsername()));
|
||||
}
|
||||
if (RegisterMode.WHITELIST.equals(registerModeService.getRegisterMode()) && !user.isApproved()) {
|
||||
if (user.getRegisterReason() != null && !user.getRegisterReason().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING"
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Register reason not approved",
|
||||
"reason_code", "NOT_APPROVED",
|
||||
"token", jwtService.generateReasonToken(user.getUsername())));
|
||||
}
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.getUsername())));
|
||||
}
|
||||
|
||||
@PostMapping("/google")
|
||||
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
||||
Optional<User> user = googleAuthService.authenticate(req.getIdToken(), registerModeService.getRegisterMode());
|
||||
if (user.isPresent()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
}
|
||||
if (!user.get().isApproved()) {
|
||||
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "NOT_APPROVED",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Invalid google token",
|
||||
"reason_code", "INVALID_CREDENTIALS"
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@PostMapping("/reason")
|
||||
public ResponseEntity<?> reason(@RequestBody MakeReasonRequest req) {
|
||||
String username = jwtService.validateAndGetSubjectForReason(req.getToken());
|
||||
Optional<User> userOpt = userService.findByUsername(username);
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Invalid token, Please re-login",
|
||||
"reason_code", "INVALID_CREDENTIALS"
|
||||
));
|
||||
}
|
||||
|
||||
if (req.reason == null || req.reason.length() <= 20) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Reason's length must longer than 20",
|
||||
"reason_code", "INVALID_CREDENTIALS"
|
||||
));
|
||||
}
|
||||
|
||||
User user = userOpt.get();
|
||||
if (user.isApproved() || registerModeService.getRegisterMode() == RegisterMode.DIRECT) {
|
||||
return ResponseEntity.ok().body(Map.of("valid", true));
|
||||
}
|
||||
|
||||
user = userService.updateReason(user.getUsername(), req.getReason());
|
||||
notificationService.createRegisterRequestNotifications(user, req.getReason());
|
||||
return ResponseEntity.ok().body(Map.of("valid", true));
|
||||
}
|
||||
|
||||
@PostMapping("/github")
|
||||
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
||||
Optional<User> user = githubAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
|
||||
if (user.isPresent()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
}
|
||||
if (!user.get().isApproved()) {
|
||||
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||
// 已填写注册理由
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "NOT_APPROVED",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Invalid github code",
|
||||
"reason_code", "INVALID_CREDENTIALS"
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/discord")
|
||||
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
||||
Optional<User> user = discordAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
|
||||
if (user.isPresent()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
}
|
||||
if (!user.get().isApproved()) {
|
||||
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "NOT_APPROVED",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Invalid discord code",
|
||||
"reason_code", "INVALID_CREDENTIALS"
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/twitter")
|
||||
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
||||
Optional<User> user = twitterAuthService.authenticate(
|
||||
req.getCode(),
|
||||
req.getCodeVerifier(),
|
||||
registerModeService.getRegisterMode(),
|
||||
req.getRedirectUri());
|
||||
if (user.isPresent()) {
|
||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
}
|
||||
if (!user.get().isApproved()) {
|
||||
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "IS_APPROVING",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Account awaiting approval",
|
||||
"reason_code", "NOT_APPROVED",
|
||||
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||
));
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"error", "Invalid twitter code",
|
||||
"reason_code", "INVALID_CREDENTIALS"
|
||||
));
|
||||
}
|
||||
|
||||
@GetMapping("/check")
|
||||
public ResponseEntity<?> checkToken() {
|
||||
return ResponseEntity.ok(Map.of("valid", true));
|
||||
}
|
||||
|
||||
@PostMapping("/forgot/send")
|
||||
public ResponseEntity<?> sendReset(@RequestBody ForgotPasswordRequest req) {
|
||||
Optional<User> userOpt = userService.findByEmail(req.getEmail());
|
||||
if (userOpt.isEmpty()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
|
||||
}
|
||||
String code = userService.generatePasswordResetCode(req.getEmail());
|
||||
emailService.sendEmail(req.getEmail(), "请填写验证码以重置密码", "您的验证码是" + code);
|
||||
return ResponseEntity.ok(Map.of("message", "Verification code sent"));
|
||||
}
|
||||
|
||||
@PostMapping("/forgot/verify")
|
||||
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
|
||||
boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode());
|
||||
if (ok) {
|
||||
String username = userService.findByEmail(req.getEmail()).get().getUsername();
|
||||
return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username)));
|
||||
}
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
||||
}
|
||||
|
||||
@PostMapping("/forgot/reset")
|
||||
public ResponseEntity<?> resetPassword(@RequestBody ResetPasswordRequest req) {
|
||||
String username = jwtService.validateAndGetSubjectForReset(req.getToken());
|
||||
try {
|
||||
userService.updatePassword(username, req.getPassword());
|
||||
return ResponseEntity.ok(Map.of("message", "Password updated"));
|
||||
} catch (FieldException e) {
|
||||
return ResponseEntity.badRequest().body(Map.of(
|
||||
"field", e.getField(),
|
||||
"error", e.getMessage()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class RegisterRequest {
|
||||
private String username;
|
||||
private String email;
|
||||
private String password;
|
||||
private String captcha;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class LoginRequest {
|
||||
private String username;
|
||||
private String password;
|
||||
private String captcha;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class GoogleLoginRequest {
|
||||
private String idToken;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class GithubLoginRequest {
|
||||
private String code;
|
||||
private String redirectUri;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class DiscordLoginRequest {
|
||||
private String code;
|
||||
private String redirectUri;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TwitterLoginRequest {
|
||||
private String code;
|
||||
private String redirectUri;
|
||||
private String codeVerifier;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class VerifyRequest {
|
||||
private String username;
|
||||
private String code;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class MakeReasonRequest {
|
||||
private String token;
|
||||
private String reason;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ForgotPasswordRequest {
|
||||
private String email;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class VerifyForgotRequest {
|
||||
private String email;
|
||||
private String code;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ResetPasswordRequest {
|
||||
private String token;
|
||||
private String password;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Category;
|
||||
import com.openisle.service.CategoryService;
|
||||
import com.openisle.service.PostService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/categories")
|
||||
@RequiredArgsConstructor
|
||||
public class CategoryController {
|
||||
private final CategoryService categoryService;
|
||||
private final PostService postService;
|
||||
|
||||
@PostMapping
|
||||
public CategoryDto create(@RequestBody CategoryRequest req) {
|
||||
Category c = categoryService.createCategory(req.getName(), req.getDescription(), req.getIcon(), req.getSmallIcon());
|
||||
long count = postService.countPostsByCategory(c.getId());
|
||||
return toDto(c, count);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
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());
|
||||
return toDto(c, count);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void delete(@PathVariable Long id) {
|
||||
categoryService.deleteCategory(id);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<CategoryDto> list() {
|
||||
return categoryService.listCategories().stream()
|
||||
.map(c -> toDto(c, postService.countPostsByCategory(c.getId())))
|
||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public CategoryDto get(@PathVariable Long id) {
|
||||
Category c = categoryService.getCategory(id);
|
||||
long count = postService.countPostsByCategory(c.getId());
|
||||
return toDto(c, count);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/posts")
|
||||
public List<PostSummaryDto> listPostsByCategory(@PathVariable Long id,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
||||
return postService.listPostsByCategories(java.util.List.of(id), page, pageSize)
|
||||
.stream()
|
||||
.map(p -> {
|
||||
PostSummaryDto dto = new PostSummaryDto();
|
||||
dto.setId(p.getId());
|
||||
dto.setTitle(p.getTitle());
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CategoryDto toDto(Category c, long count) {
|
||||
CategoryDto dto = new CategoryDto();
|
||||
dto.setId(c.getId());
|
||||
dto.setName(c.getName());
|
||||
dto.setIcon(c.getIcon());
|
||||
dto.setSmallIcon(c.getSmallIcon());
|
||||
dto.setDescription(c.getDescription());
|
||||
dto.setCount(count);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CategoryRequest {
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CategoryDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
private Long count;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PostSummaryDto {
|
||||
private Long id;
|
||||
private String title;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.service.CommentService;
|
||||
import com.openisle.service.CaptchaService;
|
||||
import com.openisle.service.LevelService;
|
||||
import com.openisle.service.ReactionService;
|
||||
import com.openisle.model.CommentSort;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
@RequiredArgsConstructor
|
||||
public class CommentController {
|
||||
private final CommentService commentService;
|
||||
private final LevelService levelService;
|
||||
private final CaptchaService captchaService;
|
||||
private final ReactionService reactionService;
|
||||
|
||||
@Value("${app.captcha.enabled:false}")
|
||||
private boolean captchaEnabled;
|
||||
|
||||
@Value("${app.captcha.comment-enabled:false}")
|
||||
private boolean commentCaptchaEnabled;
|
||||
|
||||
@PostMapping("/posts/{postId}/comments")
|
||||
public ResponseEntity<CommentDto> createComment(@PathVariable Long postId,
|
||||
@RequestBody CommentRequest req,
|
||||
Authentication auth) {
|
||||
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
Comment comment = commentService.addComment(auth.getName(), postId, req.getContent());
|
||||
CommentDto dto = toDto(comment);
|
||||
dto.setReward(levelService.awardForComment(auth.getName()));
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@PostMapping("/comments/{commentId}/replies")
|
||||
public ResponseEntity<CommentDto> replyComment(@PathVariable Long commentId,
|
||||
@RequestBody CommentRequest req,
|
||||
Authentication auth) {
|
||||
if (captchaEnabled && commentCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
Comment comment = commentService.addReply(auth.getName(), commentId, req.getContent());
|
||||
CommentDto dto = toDto(comment);
|
||||
dto.setReward(levelService.awardForComment(auth.getName()));
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@GetMapping("/posts/{postId}/comments")
|
||||
public List<CommentDto> listComments(@PathVariable Long postId,
|
||||
@RequestParam(value = "sort", required = false, defaultValue = "OLDEST") CommentSort sort) {
|
||||
return commentService.getCommentsForPost(postId, sort).stream()
|
||||
.map(this::toDtoWithReplies)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private CommentDto toDtoWithReplies(Comment comment) {
|
||||
CommentDto dto = toDto(comment);
|
||||
List<CommentDto> replies = commentService.getReplies(comment.getId()).stream()
|
||||
.map(this::toDtoWithReplies)
|
||||
.collect(Collectors.toList());
|
||||
dto.setReplies(replies);
|
||||
List<ReactionDto> reactions = reactionService.getReactionsForComment(comment.getId()).stream()
|
||||
.map(this::toReactionDto)
|
||||
.collect(Collectors.toList());
|
||||
dto.setReactions(reactions);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@DeleteMapping("/comments/{id}")
|
||||
public void deleteComment(@PathVariable Long id, Authentication auth) {
|
||||
commentService.deleteComment(auth.getName(), id);
|
||||
}
|
||||
|
||||
private CommentDto toDto(Comment comment) {
|
||||
CommentDto dto = new CommentDto();
|
||||
dto.setId(comment.getId());
|
||||
dto.setContent(comment.getContent());
|
||||
dto.setCreatedAt(comment.getCreatedAt());
|
||||
dto.setAuthor(toAuthorDto(comment.getAuthor()));
|
||||
dto.setReward(0);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private AuthorDto toAuthorDto(com.openisle.model.User user) {
|
||||
AuthorDto dto = new AuthorDto();
|
||||
dto.setId(user.getId());
|
||||
dto.setUsername(user.getUsername());
|
||||
dto.setAvatar(user.getAvatar());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CommentRequest {
|
||||
private String content;
|
||||
private String captcha;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CommentDto {
|
||||
private Long id;
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private AuthorDto author;
|
||||
private List<CommentDto> replies;
|
||||
private List<ReactionDto> reactions;
|
||||
private int reward;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class AuthorDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
private ReactionDto toReactionDto(com.openisle.model.Reaction reaction) {
|
||||
ReactionDto dto = new ReactionDto();
|
||||
dto.setId(reaction.getId());
|
||||
dto.setType(reaction.getType());
|
||||
dto.setUser(reaction.getUser().getUsername());
|
||||
if (reaction.getPost() != null) {
|
||||
dto.setPostId(reaction.getPost().getId());
|
||||
}
|
||||
if (reaction.getComment() != null) {
|
||||
dto.setCommentId(reaction.getComment().getId());
|
||||
}
|
||||
dto.setReward(0);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ReactionDto {
|
||||
private Long id;
|
||||
private com.openisle.model.ReactionType type;
|
||||
private String user;
|
||||
private Long postId;
|
||||
private Long commentId;
|
||||
private int reward;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import com.openisle.service.RegisterModeService;
|
||||
import com.openisle.model.RegisterMode;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
@lombok.RequiredArgsConstructor
|
||||
public class ConfigController {
|
||||
|
||||
@Value("${app.captcha.enabled:false}")
|
||||
private boolean captchaEnabled;
|
||||
|
||||
@Value("${app.captcha.register-enabled:false}")
|
||||
private boolean registerCaptchaEnabled;
|
||||
|
||||
@Value("${app.captcha.login-enabled:false}")
|
||||
private boolean loginCaptchaEnabled;
|
||||
|
||||
@Value("${app.captcha.post-enabled:false}")
|
||||
private boolean postCaptchaEnabled;
|
||||
|
||||
@Value("${app.captcha.comment-enabled:false}")
|
||||
private boolean commentCaptchaEnabled;
|
||||
|
||||
@Value("${app.ai.format-limit:3}")
|
||||
private int aiFormatLimit;
|
||||
|
||||
private final RegisterModeService registerModeService;
|
||||
|
||||
@GetMapping("/config")
|
||||
public ConfigResponse getConfig() {
|
||||
ConfigResponse resp = new ConfigResponse();
|
||||
resp.setCaptchaEnabled(captchaEnabled);
|
||||
resp.setRegisterCaptchaEnabled(registerCaptchaEnabled);
|
||||
resp.setLoginCaptchaEnabled(loginCaptchaEnabled);
|
||||
resp.setPostCaptchaEnabled(postCaptchaEnabled);
|
||||
resp.setCommentCaptchaEnabled(commentCaptchaEnabled);
|
||||
resp.setAiFormatLimit(aiFormatLimit);
|
||||
resp.setRegisterMode(registerModeService.getRegisterMode());
|
||||
return resp;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ConfigResponse {
|
||||
private boolean captchaEnabled;
|
||||
private boolean registerCaptchaEnabled;
|
||||
private boolean loginCaptchaEnabled;
|
||||
private boolean postCaptchaEnabled;
|
||||
private boolean commentCaptchaEnabled;
|
||||
private int aiFormatLimit;
|
||||
private RegisterMode registerMode;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Draft;
|
||||
import com.openisle.service.DraftService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/drafts")
|
||||
@RequiredArgsConstructor
|
||||
public class DraftController {
|
||||
private final DraftService draftService;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<DraftDto> saveDraft(@RequestBody DraftRequest req, Authentication auth) {
|
||||
Draft draft = draftService.saveDraft(auth.getName(), req.getCategoryId(), req.getTitle(), req.getContent(), req.getTagIds());
|
||||
return ResponseEntity.ok(toDto(draft));
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<DraftDto> getMyDraft(Authentication auth) {
|
||||
return draftService.getDraft(auth.getName())
|
||||
.map(d -> ResponseEntity.ok(toDto(d)))
|
||||
.orElseGet(() -> ResponseEntity.noContent().build());
|
||||
}
|
||||
|
||||
@DeleteMapping("/me")
|
||||
public ResponseEntity<?> deleteMyDraft(Authentication auth) {
|
||||
draftService.deleteDraft(auth.getName());
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
private DraftDto toDto(Draft draft) {
|
||||
DraftDto dto = new DraftDto();
|
||||
dto.setId(draft.getId());
|
||||
dto.setTitle(draft.getTitle());
|
||||
dto.setContent(draft.getContent());
|
||||
if (draft.getCategory() != null) {
|
||||
dto.setCategoryId(draft.getCategory().getId());
|
||||
}
|
||||
dto.setTagIds(draft.getTags().stream().map(com.openisle.model.Tag::getId).collect(Collectors.toList()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class DraftRequest {
|
||||
private String title;
|
||||
private String content;
|
||||
private Long categoryId;
|
||||
private List<Long> tagIds;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class DraftDto {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String content;
|
||||
private Long categoryId;
|
||||
private List<Long> tagIds;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||
import com.openisle.exception.FieldException;
|
||||
import com.openisle.exception.NotFoundException;
|
||||
import com.openisle.exception.RateLimitException;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RestControllerAdvice
|
||||
public class GlobalExceptionHandler {
|
||||
|
||||
@ExceptionHandler(FieldException.class)
|
||||
public ResponseEntity<?> handleFieldException(FieldException ex) {
|
||||
return ResponseEntity.badRequest()
|
||||
.body(Map.of("error", ex.getMessage(), "field", ex.getField()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(NotFoundException.class)
|
||||
public ResponseEntity<?> handleNotFoundException(NotFoundException ex) {
|
||||
return ResponseEntity.status(404).body(Map.of("error", ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(RateLimitException.class)
|
||||
public ResponseEntity<?> handleRateLimitException(RateLimitException ex) {
|
||||
return ResponseEntity.status(429).body(Map.of("error", ex.getMessage()));
|
||||
}
|
||||
|
||||
@ExceptionHandler(Exception.class)
|
||||
public ResponseEntity<?> handleException(Exception ex) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", ex.getMessage()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
public class HelloController {
|
||||
@GetMapping("/api/hello")
|
||||
public Map<String, String> hello() {
|
||||
return Map.of("message", "Hello, Authenticated User");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Notification;
|
||||
import com.openisle.model.NotificationType;
|
||||
import com.openisle.model.ReactionType;
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.service.NotificationService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/** Endpoints for user notifications. */
|
||||
@RestController
|
||||
@RequestMapping("/api/notifications")
|
||||
@RequiredArgsConstructor
|
||||
public class NotificationController {
|
||||
private final NotificationService notificationService;
|
||||
|
||||
@GetMapping
|
||||
public List<NotificationDto> list(@RequestParam(value = "read", required = false) Boolean read,
|
||||
Authentication auth) {
|
||||
return notificationService.listNotifications(auth.getName(), read).stream()
|
||||
.map(this::toDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/unread-count")
|
||||
public UnreadCount unreadCount(Authentication auth) {
|
||||
long count = notificationService.countUnread(auth.getName());
|
||||
UnreadCount uc = new UnreadCount();
|
||||
uc.setCount(count);
|
||||
return uc;
|
||||
}
|
||||
|
||||
@PostMapping("/read")
|
||||
public void markRead(@RequestBody MarkReadRequest req, Authentication auth) {
|
||||
notificationService.markRead(auth.getName(), req.getIds());
|
||||
}
|
||||
|
||||
private NotificationDto toDto(Notification n) {
|
||||
NotificationDto dto = new NotificationDto();
|
||||
dto.setId(n.getId());
|
||||
dto.setType(n.getType());
|
||||
if (n.getPost() != null) {
|
||||
dto.setPost(toPostDto(n.getPost()));
|
||||
}
|
||||
if (n.getComment() != null) {
|
||||
dto.setComment(toCommentDto(n.getComment()));
|
||||
Comment parent = n.getComment().getParent();
|
||||
if (parent != null) {
|
||||
dto.setParentComment(toCommentDto(parent));
|
||||
}
|
||||
}
|
||||
if (n.getFromUser() != null) {
|
||||
dto.setFromUser(toAuthorDto(n.getFromUser()));
|
||||
}
|
||||
if (n.getReactionType() != null) {
|
||||
dto.setReactionType(n.getReactionType());
|
||||
}
|
||||
dto.setApproved(n.getApproved());
|
||||
dto.setContent(n.getContent());
|
||||
dto.setRead(n.isRead());
|
||||
dto.setCreatedAt(n.getCreatedAt());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private PostDto toPostDto(Post post) {
|
||||
PostDto dto = new PostDto();
|
||||
dto.setId(post.getId());
|
||||
dto.setTitle(post.getTitle());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private CommentDto toCommentDto(Comment comment) {
|
||||
CommentDto dto = new CommentDto();
|
||||
dto.setId(comment.getId());
|
||||
dto.setContent(comment.getContent());
|
||||
dto.setCreatedAt(comment.getCreatedAt());
|
||||
dto.setAuthor(toAuthorDto(comment.getAuthor()));
|
||||
return dto;
|
||||
}
|
||||
|
||||
private AuthorDto toAuthorDto(com.openisle.model.User user) {
|
||||
AuthorDto dto = new AuthorDto();
|
||||
dto.setId(user.getId());
|
||||
dto.setUsername(user.getUsername());
|
||||
dto.setAvatar(user.getAvatar());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class MarkReadRequest {
|
||||
private List<Long> ids;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class NotificationDto {
|
||||
private Long id;
|
||||
private NotificationType type;
|
||||
private PostDto post;
|
||||
private CommentDto comment;
|
||||
private CommentDto parentComment;
|
||||
private AuthorDto fromUser;
|
||||
private ReactionType reactionType;
|
||||
private String content;
|
||||
private Boolean approved;
|
||||
private boolean read;
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PostDto {
|
||||
private Long id;
|
||||
private String title;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CommentDto {
|
||||
private Long id;
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private AuthorDto author;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class AuthorDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class UnreadCount {
|
||||
private long count;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.Reaction;
|
||||
import com.openisle.service.CommentService;
|
||||
import com.openisle.model.CommentSort;
|
||||
import com.openisle.service.PostService;
|
||||
import com.openisle.service.ReactionService;
|
||||
import com.openisle.service.CaptchaService;
|
||||
import com.openisle.service.DraftService;
|
||||
import com.openisle.service.SubscriptionService;
|
||||
import com.openisle.service.UserVisitService;
|
||||
import com.openisle.service.LevelService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/posts")
|
||||
@RequiredArgsConstructor
|
||||
public class PostController {
|
||||
private final PostService postService;
|
||||
private final CommentService commentService;
|
||||
private final ReactionService reactionService;
|
||||
private final SubscriptionService subscriptionService;
|
||||
private final LevelService levelService;
|
||||
private final CaptchaService captchaService;
|
||||
private final DraftService draftService;
|
||||
private final UserVisitService userVisitService;
|
||||
|
||||
@Value("${app.captcha.enabled:false}")
|
||||
private boolean captchaEnabled;
|
||||
|
||||
@Value("${app.captcha.post-enabled:false}")
|
||||
private boolean postCaptchaEnabled;
|
||||
|
||||
@PostMapping
|
||||
public ResponseEntity<PostDto> createPost(@RequestBody PostRequest req, Authentication auth) {
|
||||
if (captchaEnabled && postCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
Post post = postService.createPost(auth.getName(), req.getCategoryId(),
|
||||
req.getTitle(), req.getContent(), req.getTagIds());
|
||||
draftService.deleteDraft(auth.getName());
|
||||
PostDto dto = toDto(post);
|
||||
dto.setReward(levelService.awardForPost(auth.getName()));
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
public ResponseEntity<PostDto> updatePost(@PathVariable Long id, @RequestBody PostRequest req,
|
||||
Authentication auth) {
|
||||
Post post = postService.updatePost(id, auth.getName(), req.getCategoryId(),
|
||||
req.getTitle(), req.getContent(), req.getTagIds());
|
||||
return ResponseEntity.ok(toDto(post));
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void deletePost(@PathVariable Long id, Authentication auth) {
|
||||
postService.deletePost(id, auth.getName());
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public ResponseEntity<PostDto> getPost(@PathVariable Long id, Authentication auth) {
|
||||
String viewer = auth != null ? auth.getName() : null;
|
||||
Post post = postService.viewPost(id, viewer);
|
||||
return ResponseEntity.ok(toDto(post, viewer));
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<PostDto> listPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||
Authentication auth) {
|
||||
List<Long> ids = categoryIds;
|
||||
if (categoryId != null) {
|
||||
ids = java.util.List.of(categoryId);
|
||||
}
|
||||
List<Long> tids = tagIds;
|
||||
if (tagId != null) {
|
||||
tids = java.util.List.of(tagId);
|
||||
}
|
||||
|
||||
if (auth != null) {
|
||||
userVisitService.recordVisit(auth.getName());
|
||||
}
|
||||
|
||||
boolean hasCategories = ids != null && !ids.isEmpty();
|
||||
boolean hasTags = tids != null && !tids.isEmpty();
|
||||
|
||||
if (hasCategories && hasTags) {
|
||||
return postService.listPostsByCategoriesAndTags(ids, tids, page, pageSize)
|
||||
.stream().map(this::toDto).collect(Collectors.toList());
|
||||
}
|
||||
if (hasTags) {
|
||||
return postService.listPostsByTags(tids, page, pageSize)
|
||||
.stream().map(this::toDto).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
return postService.listPostsByCategories(ids, page, pageSize)
|
||||
.stream().map(this::toDto).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/ranking")
|
||||
public List<PostDto> rankingPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||
Authentication auth) {
|
||||
List<Long> ids = categoryIds;
|
||||
if (categoryId != null) {
|
||||
ids = java.util.List.of(categoryId);
|
||||
}
|
||||
List<Long> tids = tagIds;
|
||||
if (tagId != null) {
|
||||
tids = java.util.List.of(tagId);
|
||||
}
|
||||
|
||||
if (auth != null) {
|
||||
userVisitService.recordVisit(auth.getName());
|
||||
}
|
||||
|
||||
return postService.listPostsByViews(ids, tids, page, pageSize)
|
||||
.stream().map(this::toDto).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/latest-reply")
|
||||
public List<PostDto> latestReplyPosts(@RequestParam(value = "categoryId", required = false) Long categoryId,
|
||||
@RequestParam(value = "categoryIds", required = false) List<Long> categoryIds,
|
||||
@RequestParam(value = "tagId", required = false) Long tagId,
|
||||
@RequestParam(value = "tagIds", required = false) List<Long> tagIds,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize,
|
||||
Authentication auth) {
|
||||
List<Long> ids = categoryIds;
|
||||
if (categoryId != null) {
|
||||
ids = java.util.List.of(categoryId);
|
||||
}
|
||||
List<Long> tids = tagIds;
|
||||
if (tagId != null) {
|
||||
tids = java.util.List.of(tagId);
|
||||
}
|
||||
|
||||
if (auth != null) {
|
||||
userVisitService.recordVisit(auth.getName());
|
||||
}
|
||||
|
||||
return postService.listPostsByLatestReply(ids, tids, page, pageSize)
|
||||
.stream().map(this::toDto).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private PostDto toDto(Post post) {
|
||||
PostDto dto = new PostDto();
|
||||
dto.setId(post.getId());
|
||||
dto.setTitle(post.getTitle());
|
||||
dto.setContent(post.getContent());
|
||||
dto.setCreatedAt(post.getCreatedAt());
|
||||
dto.setAuthor(toAuthorDto(post.getAuthor()));
|
||||
dto.setCategory(toCategoryDto(post.getCategory()));
|
||||
dto.setTags(post.getTags().stream().map(this::toTagDto).collect(Collectors.toList()));
|
||||
dto.setViews(post.getViews());
|
||||
dto.setStatus(post.getStatus());
|
||||
dto.setPinnedAt(post.getPinnedAt());
|
||||
|
||||
List<ReactionDto> reactions = reactionService.getReactionsForPost(post.getId())
|
||||
.stream()
|
||||
.map(this::toReactionDto)
|
||||
.collect(Collectors.toList());
|
||||
dto.setReactions(reactions);
|
||||
|
||||
List<CommentDto> comments = commentService.getCommentsForPost(post.getId(), CommentSort.OLDEST)
|
||||
.stream()
|
||||
.map(this::toCommentDtoWithReplies)
|
||||
.collect(Collectors.toList());
|
||||
dto.setComments(comments);
|
||||
|
||||
java.util.List<com.openisle.model.User> participants = commentService.getParticipants(post.getId(), 5);
|
||||
dto.setParticipants(participants.stream().map(this::toAuthorDto).collect(Collectors.toList()));
|
||||
|
||||
java.time.LocalDateTime last = commentService.getLastCommentTime(post.getId());
|
||||
dto.setLastReplyAt(last != null ? last : post.getCreatedAt());
|
||||
dto.setReward(0);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private PostDto toDto(Post post, String viewer) {
|
||||
PostDto dto = toDto(post);
|
||||
if (viewer != null) {
|
||||
dto.setSubscribed(subscriptionService.isPostSubscribed(viewer, post.getId()));
|
||||
} else {
|
||||
dto.setSubscribed(false);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private CommentDto toCommentDtoWithReplies(Comment comment) {
|
||||
CommentDto dto = toCommentDto(comment);
|
||||
List<CommentDto> replies = commentService.getReplies(comment.getId()).stream()
|
||||
.map(this::toCommentDtoWithReplies)
|
||||
.collect(Collectors.toList());
|
||||
dto.setReplies(replies);
|
||||
|
||||
List<ReactionDto> reactions = reactionService.getReactionsForComment(comment.getId())
|
||||
.stream()
|
||||
.map(this::toReactionDto)
|
||||
.collect(Collectors.toList());
|
||||
dto.setReactions(reactions);
|
||||
|
||||
return dto;
|
||||
}
|
||||
|
||||
private CommentDto toCommentDto(Comment comment) {
|
||||
CommentDto dto = new CommentDto();
|
||||
dto.setId(comment.getId());
|
||||
dto.setContent(comment.getContent());
|
||||
dto.setCreatedAt(comment.getCreatedAt());
|
||||
dto.setAuthor(toAuthorDto(comment.getAuthor()));
|
||||
dto.setReward(0);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private ReactionDto toReactionDto(Reaction reaction) {
|
||||
ReactionDto dto = new ReactionDto();
|
||||
dto.setId(reaction.getId());
|
||||
dto.setType(reaction.getType());
|
||||
dto.setUser(reaction.getUser().getUsername());
|
||||
if (reaction.getPost() != null) {
|
||||
dto.setPostId(reaction.getPost().getId());
|
||||
}
|
||||
if (reaction.getComment() != null) {
|
||||
dto.setCommentId(reaction.getComment().getId());
|
||||
}
|
||||
dto.setReward(0);
|
||||
return dto;
|
||||
}
|
||||
|
||||
private CategoryDto toCategoryDto(com.openisle.model.Category category) {
|
||||
CategoryDto dto = new CategoryDto();
|
||||
dto.setId(category.getId());
|
||||
dto.setName(category.getName());
|
||||
dto.setDescription(category.getDescription());
|
||||
dto.setIcon(category.getIcon());
|
||||
dto.setSmallIcon(category.getSmallIcon());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private TagDto toTagDto(com.openisle.model.Tag tag) {
|
||||
TagDto dto = new TagDto();
|
||||
dto.setId(tag.getId());
|
||||
dto.setName(tag.getName());
|
||||
dto.setDescription(tag.getDescription());
|
||||
dto.setIcon(tag.getIcon());
|
||||
dto.setSmallIcon(tag.getSmallIcon());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private AuthorDto toAuthorDto(com.openisle.model.User user) {
|
||||
AuthorDto dto = new AuthorDto();
|
||||
dto.setId(user.getId());
|
||||
dto.setUsername(user.getUsername());
|
||||
dto.setAvatar(user.getAvatar());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PostRequest {
|
||||
private Long categoryId;
|
||||
private String title;
|
||||
private String content;
|
||||
private java.util.List<Long> tagIds;
|
||||
private String captcha;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PostDto {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private AuthorDto author;
|
||||
private CategoryDto category;
|
||||
private java.util.List<TagDto> tags;
|
||||
private long views;
|
||||
private com.openisle.model.PostStatus status;
|
||||
private LocalDateTime pinnedAt;
|
||||
private LocalDateTime lastReplyAt;
|
||||
private List<CommentDto> comments;
|
||||
private List<ReactionDto> reactions;
|
||||
private java.util.List<AuthorDto> participants;
|
||||
private boolean subscribed;
|
||||
private int reward;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CategoryDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TagDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class AuthorDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CommentDto {
|
||||
private Long id;
|
||||
private String content;
|
||||
private LocalDateTime createdAt;
|
||||
private AuthorDto author;
|
||||
private List<CommentDto> replies;
|
||||
private List<ReactionDto> reactions;
|
||||
private int reward;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ReactionDto {
|
||||
private Long id;
|
||||
private com.openisle.model.ReactionType type;
|
||||
private String user;
|
||||
private Long postId;
|
||||
private Long commentId;
|
||||
private int reward;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.service.PushSubscriptionService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/push")
|
||||
@RequiredArgsConstructor
|
||||
public class PushSubscriptionController {
|
||||
private final PushSubscriptionService pushSubscriptionService;
|
||||
@Value("${app.webpush.public-key}")
|
||||
private String publicKey;
|
||||
|
||||
@GetMapping("/public-key")
|
||||
public PublicKeyResponse getPublicKey() {
|
||||
PublicKeyResponse r = new PublicKeyResponse();
|
||||
r.setKey(publicKey);
|
||||
return r;
|
||||
}
|
||||
|
||||
@PostMapping("/subscribe")
|
||||
public void subscribe(@RequestBody SubscriptionRequest req, Authentication auth) {
|
||||
pushSubscriptionService.saveSubscription(auth.getName(), req.getEndpoint(), req.getP256dh(), req.getAuth());
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PublicKeyResponse {
|
||||
private String key;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class SubscriptionRequest {
|
||||
private String endpoint;
|
||||
private String p256dh;
|
||||
private String auth;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Reaction;
|
||||
import com.openisle.model.ReactionType;
|
||||
import com.openisle.service.ReactionService;
|
||||
import com.openisle.service.LevelService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api")
|
||||
@RequiredArgsConstructor
|
||||
public class ReactionController {
|
||||
private final ReactionService reactionService;
|
||||
private final LevelService levelService;
|
||||
|
||||
/**
|
||||
* Get all available reaction types.
|
||||
*/
|
||||
@GetMapping("/reaction-types")
|
||||
public ReactionType[] listReactionTypes() {
|
||||
return ReactionType.values();
|
||||
}
|
||||
|
||||
@PostMapping("/posts/{postId}/reactions")
|
||||
public ResponseEntity<ReactionDto> reactToPost(@PathVariable Long postId,
|
||||
@RequestBody ReactionRequest req,
|
||||
Authentication auth) {
|
||||
Reaction reaction = reactionService.reactToPost(auth.getName(), postId, req.getType());
|
||||
if (reaction == null) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
ReactionDto dto = toDto(reaction);
|
||||
dto.setReward(levelService.awardForReaction(auth.getName()));
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
@PostMapping("/comments/{commentId}/reactions")
|
||||
public ResponseEntity<ReactionDto> reactToComment(@PathVariable Long commentId,
|
||||
@RequestBody ReactionRequest req,
|
||||
Authentication auth) {
|
||||
Reaction reaction = reactionService.reactToComment(auth.getName(), commentId, req.getType());
|
||||
if (reaction == null) {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
ReactionDto dto = toDto(reaction);
|
||||
dto.setReward(levelService.awardForReaction(auth.getName()));
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
private ReactionDto toDto(Reaction reaction) {
|
||||
ReactionDto dto = new ReactionDto();
|
||||
dto.setId(reaction.getId());
|
||||
dto.setType(reaction.getType());
|
||||
dto.setUser(reaction.getUser().getUsername());
|
||||
if (reaction.getPost() != null) {
|
||||
dto.setPostId(reaction.getPost().getId());
|
||||
}
|
||||
if (reaction.getComment() != null) {
|
||||
dto.setCommentId(reaction.getComment().getId());
|
||||
}
|
||||
dto.setReward(0);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ReactionRequest {
|
||||
private ReactionType type;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ReactionDto {
|
||||
private Long id;
|
||||
private ReactionType type;
|
||||
private String user;
|
||||
private Long postId;
|
||||
private Long commentId;
|
||||
private int reward;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.Comment;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.service.SearchService;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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 java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/search")
|
||||
@RequiredArgsConstructor
|
||||
public class SearchController {
|
||||
private final SearchService searchService;
|
||||
|
||||
@GetMapping("/users")
|
||||
public List<UserDto> searchUsers(@RequestParam String keyword) {
|
||||
return searchService.searchUsers(keyword).stream()
|
||||
.map(this::toUserDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/posts")
|
||||
public List<PostDto> searchPosts(@RequestParam String keyword) {
|
||||
return searchService.searchPosts(keyword).stream()
|
||||
.map(this::toPostDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/posts/content")
|
||||
public List<PostDto> searchPostsByContent(@RequestParam String keyword) {
|
||||
return searchService.searchPostsByContent(keyword).stream()
|
||||
.map(this::toPostDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/posts/title")
|
||||
public List<PostDto> searchPostsByTitle(@RequestParam String keyword) {
|
||||
return searchService.searchPostsByTitle(keyword).stream()
|
||||
.map(this::toPostDto)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/global")
|
||||
public List<SearchResultDto> global(@RequestParam String keyword) {
|
||||
return searchService.globalSearch(keyword).stream()
|
||||
.map(r -> {
|
||||
SearchResultDto dto = new SearchResultDto();
|
||||
dto.setType(r.type());
|
||||
dto.setId(r.id());
|
||||
dto.setText(r.text());
|
||||
dto.setSubText(r.subText());
|
||||
dto.setExtra(r.extra());
|
||||
dto.setPostId(r.postId());
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private UserDto toUserDto(User user) {
|
||||
UserDto dto = new UserDto();
|
||||
dto.setId(user.getId());
|
||||
dto.setUsername(user.getUsername());
|
||||
dto.setAvatar(user.getAvatar());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private PostDto toPostDto(Post post) {
|
||||
PostDto dto = new PostDto();
|
||||
dto.setId(post.getId());
|
||||
dto.setTitle(post.getTitle());
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class UserDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PostDto {
|
||||
private Long id;
|
||||
private String title;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class SearchResultDto {
|
||||
private String type;
|
||||
private Long id;
|
||||
private String text;
|
||||
private String subText;
|
||||
private String extra;
|
||||
private Long postId;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Post;
|
||||
import com.openisle.model.PostStatus;
|
||||
import com.openisle.repository.PostRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.MediaType;
|
||||
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 java.util.List;
|
||||
|
||||
/**
|
||||
* Controller for dynamic sitemap generation.
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/api")
|
||||
public class SitemapController {
|
||||
private final PostRepository postRepository;
|
||||
|
||||
@Value("${app.website-url:https://www.open-isle.com}")
|
||||
private String websiteUrl;
|
||||
|
||||
@GetMapping(value = "/sitemap.xml", produces = MediaType.APPLICATION_XML_VALUE)
|
||||
public ResponseEntity<String> sitemap() {
|
||||
List<Post> posts = postRepository.findByStatus(PostStatus.PUBLISHED);
|
||||
|
||||
StringBuilder body = new StringBuilder();
|
||||
body.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
|
||||
body.append("<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n");
|
||||
|
||||
List<String> staticRoutes = List.of(
|
||||
"/",
|
||||
"/about",
|
||||
"/activities",
|
||||
"/login",
|
||||
"/signup"
|
||||
);
|
||||
|
||||
for (String path : staticRoutes) {
|
||||
body.append(" <url><loc>")
|
||||
.append(websiteUrl)
|
||||
.append(path)
|
||||
.append("</loc></url>\n");
|
||||
}
|
||||
|
||||
for (Post p : posts) {
|
||||
body.append(" <url>\n")
|
||||
.append(" <loc>")
|
||||
.append(websiteUrl)
|
||||
.append("/posts/")
|
||||
.append(p.getId())
|
||||
.append("</loc>\n")
|
||||
.append(" <lastmod>")
|
||||
.append(p.getCreatedAt().toLocalDate())
|
||||
.append("</lastmod>\n")
|
||||
.append(" </url>\n");
|
||||
}
|
||||
|
||||
body.append("</urlset>");
|
||||
return ResponseEntity.ok()
|
||||
.contentType(MediaType.APPLICATION_XML)
|
||||
.body(body.toString());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.service.UserVisitService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
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 java.time.LocalDate;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/stats")
|
||||
@RequiredArgsConstructor
|
||||
public class StatController {
|
||||
private final UserVisitService userVisitService;
|
||||
|
||||
@GetMapping("/dau")
|
||||
public Map<String, Long> dau(@RequestParam(value = "date", required = false)
|
||||
@DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate date) {
|
||||
long count = userVisitService.countDau(date);
|
||||
return Map.of("dau", count);
|
||||
}
|
||||
|
||||
@GetMapping("/dau-range")
|
||||
public List<Map<String, Object>> dauRange(@RequestParam(value = "days", defaultValue = "30") int days) {
|
||||
if (days < 1) days = 1;
|
||||
LocalDate end = LocalDate.now();
|
||||
LocalDate start = end.minusDays(days - 1L);
|
||||
var data = userVisitService.countDauRange(start, end);
|
||||
return data.entrySet().stream()
|
||||
.map(e -> Map.<String,Object>of(
|
||||
"date", e.getKey().toString(),
|
||||
"value", e.getValue()
|
||||
))
|
||||
.toList();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.service.SubscriptionService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/** Endpoints for subscribing to posts, comments and users. */
|
||||
@RestController
|
||||
@RequestMapping("/api/subscriptions")
|
||||
@RequiredArgsConstructor
|
||||
public class SubscriptionController {
|
||||
private final SubscriptionService subscriptionService;
|
||||
|
||||
@PostMapping("/posts/{postId}")
|
||||
public void subscribePost(@PathVariable Long postId, Authentication auth) {
|
||||
subscriptionService.subscribePost(auth.getName(), postId);
|
||||
}
|
||||
|
||||
@DeleteMapping("/posts/{postId}")
|
||||
public void unsubscribePost(@PathVariable Long postId, Authentication auth) {
|
||||
subscriptionService.unsubscribePost(auth.getName(), postId);
|
||||
}
|
||||
|
||||
@PostMapping("/comments/{commentId}")
|
||||
public void subscribeComment(@PathVariable Long commentId, Authentication auth) {
|
||||
subscriptionService.subscribeComment(auth.getName(), commentId);
|
||||
}
|
||||
|
||||
@DeleteMapping("/comments/{commentId}")
|
||||
public void unsubscribeComment(@PathVariable Long commentId, Authentication auth) {
|
||||
subscriptionService.unsubscribeComment(auth.getName(), commentId);
|
||||
}
|
||||
|
||||
@PostMapping("/users/{username}")
|
||||
public void subscribeUser(@PathVariable String username, Authentication auth) {
|
||||
subscriptionService.subscribeUser(auth.getName(), username);
|
||||
}
|
||||
|
||||
@DeleteMapping("/users/{username}")
|
||||
public void unsubscribeUser(@PathVariable String username, Authentication auth) {
|
||||
subscriptionService.unsubscribeUser(auth.getName(), username);
|
||||
}
|
||||
}
|
||||
125
backend/src/main/java/com/openisle/controller/TagController.java
Normal file
125
backend/src/main/java/com/openisle/controller/TagController.java
Normal file
@@ -0,0 +1,125 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.model.Tag;
|
||||
import com.openisle.service.TagService;
|
||||
import com.openisle.service.PostService;
|
||||
import com.openisle.repository.UserRepository;
|
||||
import com.openisle.model.PublishMode;
|
||||
import com.openisle.model.Role;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/tags")
|
||||
@RequiredArgsConstructor
|
||||
public class TagController {
|
||||
private final TagService tagService;
|
||||
private final PostService postService;
|
||||
private final UserRepository userRepository;
|
||||
|
||||
@PostMapping
|
||||
public TagDto create(@RequestBody TagRequest req, org.springframework.security.core.Authentication auth) {
|
||||
boolean approved = true;
|
||||
if (postService.getPublishMode() == PublishMode.REVIEW && auth != null) {
|
||||
com.openisle.model.User user = userRepository.findByUsername(auth.getName()).orElseThrow();
|
||||
if (user.getRole() != Role.ADMIN) {
|
||||
approved = false;
|
||||
}
|
||||
}
|
||||
Tag tag = tagService.createTag(
|
||||
req.getName(),
|
||||
req.getDescription(),
|
||||
req.getIcon(),
|
||||
req.getSmallIcon(),
|
||||
approved,
|
||||
auth != null ? auth.getName() : null);
|
||||
long count = postService.countPostsByTag(tag.getId());
|
||||
return toDto(tag, count);
|
||||
}
|
||||
|
||||
@PutMapping("/{id}")
|
||||
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());
|
||||
return toDto(tag, count);
|
||||
}
|
||||
|
||||
@DeleteMapping("/{id}")
|
||||
public void delete(@PathVariable Long id) {
|
||||
tagService.deleteTag(id);
|
||||
}
|
||||
|
||||
@GetMapping
|
||||
public List<TagDto> list(@RequestParam(value = "keyword", required = false) String keyword,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
List<TagDto> dtos = tagService.searchTags(keyword).stream()
|
||||
.map(t -> toDto(t, postService.countPostsByTag(t.getId())))
|
||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||
.collect(Collectors.toList());
|
||||
if (limit != null && limit > 0 && dtos.size() > limit) {
|
||||
return dtos.subList(0, limit);
|
||||
}
|
||||
return dtos;
|
||||
}
|
||||
|
||||
@GetMapping("/{id}")
|
||||
public TagDto get(@PathVariable Long id) {
|
||||
Tag tag = tagService.getTag(id);
|
||||
long count = postService.countPostsByTag(tag.getId());
|
||||
return toDto(tag, count);
|
||||
}
|
||||
|
||||
@GetMapping("/{id}/posts")
|
||||
public List<PostSummaryDto> listPostsByTag(@PathVariable Long id,
|
||||
@RequestParam(value = "page", required = false) Integer page,
|
||||
@RequestParam(value = "pageSize", required = false) Integer pageSize) {
|
||||
return postService.listPostsByTags(java.util.List.of(id), page, pageSize)
|
||||
.stream()
|
||||
.map(p -> {
|
||||
PostSummaryDto dto = new PostSummaryDto();
|
||||
dto.setId(p.getId());
|
||||
dto.setTitle(p.getTitle());
|
||||
return dto;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
private TagDto toDto(Tag tag, long count) {
|
||||
TagDto dto = new TagDto();
|
||||
dto.setId(tag.getId());
|
||||
dto.setName(tag.getName());
|
||||
dto.setIcon(tag.getIcon());
|
||||
dto.setSmallIcon(tag.getSmallIcon());
|
||||
dto.setDescription(tag.getDescription());
|
||||
dto.setCount(count);
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TagRequest {
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TagDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
private Long count;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PostSummaryDto {
|
||||
private Long id;
|
||||
private String title;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.service.ImageUploader;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
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 java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.net.URLConnection;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/upload")
|
||||
@RequiredArgsConstructor
|
||||
public class UploadController {
|
||||
private final ImageUploader imageUploader;
|
||||
|
||||
@Value("${app.upload.check-type:true}")
|
||||
private boolean checkImageType;
|
||||
|
||||
@Value("${app.upload.max-size:5242880}")
|
||||
private long maxUploadSize;
|
||||
|
||||
@PostMapping
|
||||
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"));
|
||||
}
|
||||
if (file.getSize() > maxUploadSize) {
|
||||
return ResponseEntity.badRequest().body(Map.of("code", 2, "msg", "File too large"));
|
||||
}
|
||||
String url;
|
||||
try {
|
||||
url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join();
|
||||
} catch (IOException e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
||||
}
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"code", 0,
|
||||
"msg", "ok",
|
||||
"data", Map.of("url", url)
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/url")
|
||||
public ResponseEntity<?> uploadUrl(@RequestBody Map<String, String> body) {
|
||||
String link = body.get("url");
|
||||
if (link == null || link.isBlank()) {
|
||||
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "Missing url"));
|
||||
}
|
||||
try {
|
||||
URL u = URI.create(link).toURL();
|
||||
byte[] data = u.openStream().readAllBytes();
|
||||
if (data.length > maxUploadSize) {
|
||||
return ResponseEntity.badRequest().body(Map.of("code", 2, "msg", "File too large"));
|
||||
}
|
||||
String filename = link.substring(link.lastIndexOf('/') + 1);
|
||||
String contentType = URLConnection.guessContentTypeFromStream(new ByteArrayInputStream(data));
|
||||
if (checkImageType && (contentType == null || !contentType.startsWith("image/"))) {
|
||||
return ResponseEntity.badRequest().body(Map.of("code", 1, "msg", "File is not an image"));
|
||||
}
|
||||
String url = imageUploader.upload(data, filename).join();
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"code", 0,
|
||||
"msg", "ok",
|
||||
"data", Map.of("url", url)
|
||||
));
|
||||
} catch (Exception e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("code", 3, "msg", "Upload failed"));
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/presign")
|
||||
public java.util.Map<String, String> presign(@RequestParam("filename") String filename) {
|
||||
return imageUploader.presignUpload(filename);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,369 @@
|
||||
package com.openisle.controller;
|
||||
|
||||
import com.openisle.exception.NotFoundException;
|
||||
import com.openisle.model.User;
|
||||
import com.openisle.service.*;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import lombok.Data;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/api/users")
|
||||
@RequiredArgsConstructor
|
||||
public class UserController {
|
||||
private final UserService userService;
|
||||
private final ImageUploader imageUploader;
|
||||
private final PostService postService;
|
||||
private final CommentService commentService;
|
||||
private final ReactionService reactionService;
|
||||
private final TagService tagService;
|
||||
private final SubscriptionService subscriptionService;
|
||||
private final PostReadService postReadService;
|
||||
private final UserVisitService userVisitService;
|
||||
private final LevelService levelService;
|
||||
private final JwtService jwtService;
|
||||
|
||||
@Value("${app.upload.check-type:true}")
|
||||
private boolean checkImageType;
|
||||
|
||||
@Value("${app.upload.max-size:5242880}")
|
||||
private long maxUploadSize;
|
||||
|
||||
@Value("${app.user.posts-limit:10}")
|
||||
private int defaultPostsLimit;
|
||||
|
||||
@Value("${app.user.replies-limit:50}")
|
||||
private int defaultRepliesLimit;
|
||||
|
||||
@Value("${app.user.tags-limit:50}")
|
||||
private int defaultTagsLimit;
|
||||
|
||||
@Value("${app.snippet-length:50}")
|
||||
private int snippetLength;
|
||||
|
||||
@GetMapping("/me")
|
||||
public ResponseEntity<UserDto> me(Authentication auth) {
|
||||
User user = userService.findByUsername(auth.getName()).orElseThrow();
|
||||
return ResponseEntity.ok(toDto(user, auth));
|
||||
}
|
||||
|
||||
@PostMapping("/me/avatar")
|
||||
public ResponseEntity<?> uploadAvatar(@RequestParam("file") MultipartFile file,
|
||||
Authentication auth) {
|
||||
if (checkImageType && (file.getContentType() == null || !file.getContentType().startsWith("image/"))) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "File is not an image"));
|
||||
}
|
||||
if (file.getSize() > maxUploadSize) {
|
||||
return ResponseEntity.badRequest().body(Map.of("error", "File too large"));
|
||||
}
|
||||
String url = null;
|
||||
try {
|
||||
url = imageUploader.upload(file.getBytes(), file.getOriginalFilename()).join();
|
||||
} catch (IOException e) {
|
||||
return ResponseEntity.internalServerError().body(Map.of("url", url));
|
||||
}
|
||||
userService.updateAvatar(auth.getName(), url);
|
||||
return ResponseEntity.ok(Map.of("url", url));
|
||||
}
|
||||
|
||||
@PutMapping("/me")
|
||||
public ResponseEntity<?> updateProfile(@RequestBody UpdateProfileDto dto,
|
||||
Authentication auth) {
|
||||
User user = userService.updateProfile(auth.getName(), dto.getUsername(), dto.getIntroduction());
|
||||
return ResponseEntity.ok(Map.of(
|
||||
"token", jwtService.generateToken(user.getUsername()),
|
||||
"user", toDto(user, auth)
|
||||
));
|
||||
}
|
||||
|
||||
@PostMapping("/me/signin")
|
||||
public Map<String, Integer> signIn(Authentication auth) {
|
||||
int reward = levelService.awardForSignin(auth.getName());
|
||||
return Map.of("reward", reward);
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}")
|
||||
public ResponseEntity<UserDto> getUser(@PathVariable("identifier") String identifier,
|
||||
Authentication auth) {
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow(() -> new NotFoundException("User not found"));
|
||||
return ResponseEntity.ok(toDto(user, auth));
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/posts")
|
||||
public java.util.List<PostMetaDto> userPosts(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : defaultPostsLimit;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return postService.getRecentPostsByUser(user.getUsername(), l).stream()
|
||||
.map(this::toMetaDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/replies")
|
||||
public java.util.List<CommentInfoDto> userReplies(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : defaultRepliesLimit;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return commentService.getRecentCommentsByUser(user.getUsername(), l).stream()
|
||||
.map(this::toCommentInfoDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/hot-posts")
|
||||
public java.util.List<PostMetaDto> hotPosts(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : 10;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
java.util.List<Long> ids = reactionService.topPostIds(user.getUsername(), l);
|
||||
return postService.getPostsByIds(ids).stream()
|
||||
.map(this::toMetaDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/hot-replies")
|
||||
public java.util.List<CommentInfoDto> hotReplies(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : 10;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
java.util.List<Long> ids = reactionService.topCommentIds(user.getUsername(), l);
|
||||
return commentService.getCommentsByIds(ids).stream()
|
||||
.map(this::toCommentInfoDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/hot-tags")
|
||||
public java.util.List<TagInfoDto> hotTags(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : 10;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return tagService.getTagsByUser(user.getUsername()).stream()
|
||||
.map(t -> {
|
||||
TagInfoDto dto = new TagInfoDto();
|
||||
dto.setId(t.getId());
|
||||
dto.setName(t.getName());
|
||||
dto.setDescription(t.getDescription());
|
||||
dto.setIcon(t.getIcon());
|
||||
dto.setSmallIcon(t.getSmallIcon());
|
||||
dto.setCreatedAt(t.getCreatedAt());
|
||||
dto.setCount(postService.countPostsByTag(t.getId()));
|
||||
return dto;
|
||||
})
|
||||
.sorted((a, b) -> Long.compare(b.getCount(), a.getCount()))
|
||||
.limit(l)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/tags")
|
||||
public java.util.List<TagInfoDto> userTags(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
int l = limit != null ? limit : defaultTagsLimit;
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return tagService.getRecentTagsByUser(user.getUsername(), l).stream()
|
||||
.map(t -> {
|
||||
TagInfoDto dto = new TagInfoDto();
|
||||
dto.setId(t.getId());
|
||||
dto.setName(t.getName());
|
||||
dto.setDescription(t.getDescription());
|
||||
dto.setIcon(t.getIcon());
|
||||
dto.setSmallIcon(t.getSmallIcon());
|
||||
dto.setCreatedAt(t.getCreatedAt());
|
||||
dto.setCount(postService.countPostsByTag(t.getId()));
|
||||
return dto;
|
||||
})
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/following")
|
||||
public java.util.List<UserDto> following(@PathVariable("identifier") String identifier) {
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return subscriptionService.getSubscribedUsers(user.getUsername()).stream()
|
||||
.map(this::toDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/followers")
|
||||
public java.util.List<UserDto> followers(@PathVariable("identifier") String identifier) {
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
return subscriptionService.getSubscribers(user.getUsername()).stream()
|
||||
.map(this::toDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
/**
|
||||
* List all administrator users.
|
||||
*/
|
||||
@GetMapping("/admins")
|
||||
public java.util.List<UserDto> admins() {
|
||||
return userService.getAdmins().stream()
|
||||
.map(this::toDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping("/{identifier}/all")
|
||||
public ResponseEntity<UserAggregateDto> userAggregate(@PathVariable("identifier") String identifier,
|
||||
@RequestParam(value = "postsLimit", required = false) Integer postsLimit,
|
||||
@RequestParam(value = "repliesLimit", required = false) Integer repliesLimit,
|
||||
Authentication auth) {
|
||||
User user = userService.findByIdentifier(identifier).orElseThrow();
|
||||
int pLimit = postsLimit != null ? postsLimit : defaultPostsLimit;
|
||||
int rLimit = repliesLimit != null ? repliesLimit : defaultRepliesLimit;
|
||||
java.util.List<PostMetaDto> posts = postService.getRecentPostsByUser(user.getUsername(), pLimit).stream()
|
||||
.map(this::toMetaDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
java.util.List<CommentInfoDto> replies = commentService.getRecentCommentsByUser(user.getUsername(), rLimit).stream()
|
||||
.map(this::toCommentInfoDto)
|
||||
.collect(java.util.stream.Collectors.toList());
|
||||
UserAggregateDto dto = new UserAggregateDto();
|
||||
dto.setUser(toDto(user, auth));
|
||||
dto.setPosts(posts);
|
||||
dto.setReplies(replies);
|
||||
return ResponseEntity.ok(dto);
|
||||
}
|
||||
|
||||
private UserDto toDto(User user, Authentication viewer) {
|
||||
UserDto dto = new UserDto();
|
||||
dto.setId(user.getId());
|
||||
dto.setUsername(user.getUsername());
|
||||
dto.setEmail(user.getEmail());
|
||||
dto.setAvatar(user.getAvatar());
|
||||
dto.setRole(user.getRole().name());
|
||||
dto.setIntroduction(user.getIntroduction());
|
||||
dto.setFollowers(subscriptionService.countSubscribers(user.getUsername()));
|
||||
dto.setFollowing(subscriptionService.countSubscribed(user.getUsername()));
|
||||
dto.setCreatedAt(user.getCreatedAt());
|
||||
dto.setLastPostTime(postService.getLastPostTime(user.getUsername()));
|
||||
dto.setTotalViews(postService.getTotalViews(user.getUsername()));
|
||||
dto.setVisitedDays(userVisitService.countVisits(user.getUsername()));
|
||||
dto.setReadPosts(postReadService.countReads(user.getUsername()));
|
||||
dto.setLikesSent(reactionService.countLikesSent(user.getUsername()));
|
||||
dto.setLikesReceived(reactionService.countLikesReceived(user.getUsername()));
|
||||
dto.setExperience(user.getExperience());
|
||||
dto.setCurrentLevel(levelService.getLevel(user.getExperience()));
|
||||
dto.setNextLevelExp(levelService.nextLevelExp(user.getExperience()));
|
||||
if (viewer != null) {
|
||||
dto.setSubscribed(subscriptionService.isSubscribed(viewer.getName(), user.getUsername()));
|
||||
} else {
|
||||
dto.setSubscribed(false);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
private UserDto toDto(User user) {
|
||||
return toDto(user, null);
|
||||
}
|
||||
|
||||
private PostMetaDto toMetaDto(com.openisle.model.Post post) {
|
||||
PostMetaDto dto = new PostMetaDto();
|
||||
dto.setId(post.getId());
|
||||
dto.setTitle(post.getTitle());
|
||||
String content = post.getContent();
|
||||
if (content == null) {
|
||||
content = "";
|
||||
}
|
||||
if (snippetLength >= 0) {
|
||||
dto.setSnippet(content.length() > snippetLength ? content.substring(0, snippetLength) : content);
|
||||
} else {
|
||||
dto.setSnippet(content);
|
||||
}
|
||||
dto.setCreatedAt(post.getCreatedAt());
|
||||
dto.setCategory(post.getCategory().getName());
|
||||
dto.setViews(post.getViews());
|
||||
return dto;
|
||||
}
|
||||
|
||||
private CommentInfoDto toCommentInfoDto(com.openisle.model.Comment comment) {
|
||||
CommentInfoDto dto = new CommentInfoDto();
|
||||
dto.setId(comment.getId());
|
||||
dto.setContent(comment.getContent());
|
||||
dto.setCreatedAt(comment.getCreatedAt());
|
||||
dto.setPost(toMetaDto(comment.getPost()));
|
||||
if (comment.getParent() != null) {
|
||||
ParentCommentDto pc = new ParentCommentDto();
|
||||
pc.setId(comment.getParent().getId());
|
||||
pc.setAuthor(comment.getParent().getAuthor().getUsername());
|
||||
pc.setContent(comment.getParent().getContent());
|
||||
dto.setParentComment(pc);
|
||||
}
|
||||
return dto;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class UserDto {
|
||||
private Long id;
|
||||
private String username;
|
||||
private String email;
|
||||
private String avatar;
|
||||
private String role;
|
||||
private String introduction;
|
||||
private long followers;
|
||||
private long following;
|
||||
private java.time.LocalDateTime createdAt;
|
||||
private java.time.LocalDateTime lastPostTime;
|
||||
private long totalViews;
|
||||
private long visitedDays;
|
||||
private long readPosts;
|
||||
private long likesSent;
|
||||
private long likesReceived;
|
||||
private boolean subscribed;
|
||||
private int experience;
|
||||
private int currentLevel;
|
||||
private int nextLevelExp;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class PostMetaDto {
|
||||
private Long id;
|
||||
private String title;
|
||||
private String snippet;
|
||||
private java.time.LocalDateTime createdAt;
|
||||
private String category;
|
||||
private long views;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class CommentInfoDto {
|
||||
private Long id;
|
||||
private String content;
|
||||
private java.time.LocalDateTime createdAt;
|
||||
private PostMetaDto post;
|
||||
private ParentCommentDto parentComment;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class TagInfoDto {
|
||||
private Long id;
|
||||
private String name;
|
||||
private String description;
|
||||
private String icon;
|
||||
private String smallIcon;
|
||||
private java.time.LocalDateTime createdAt;
|
||||
private Long count;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class ParentCommentDto {
|
||||
private Long id;
|
||||
private String author;
|
||||
private String content;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class UpdateProfileDto {
|
||||
private String username;
|
||||
private String introduction;
|
||||
}
|
||||
|
||||
@Data
|
||||
private static class UserAggregateDto {
|
||||
private UserDto user;
|
||||
private java.util.List<PostMetaDto> posts;
|
||||
private java.util.List<CommentInfoDto> replies;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user