Compare commits

..

27 Commits

Author SHA1 Message Date
Tim 85e77c265e feat(docs): remove /docs prefix 2025-09-09 14:03:04 +08:00
tim 358c53338d Revert "fix: 新增检查"
This reverts commit 1cd89eaa54.
2025-09-09 13:23:30 +08:00
tim 1cd89eaa54 fix: 新增检查 2025-09-09 13:16:52 +08:00
tim 1d2e7eb96e Revert "Update deploy-docs.yml"
This reverts commit 4428e06f1d.
2025-09-09 13:10:46 +08:00
Tim 4428e06f1d Update deploy-docs.yml 2025-09-09 13:03:08 +08:00
Tim dddff54556 Update README.md 2025-09-09 12:18:10 +08:00
Tim e7f7bbac22 Update README.md 2025-09-09 12:17:49 +08:00
Tim 37aae4ba5c Update README.md 2025-09-09 12:17:24 +08:00
Tim 54cfc98336 Merge pull request #945 from nagisa77/codex/fix-server-url-in-api-docs
Add configurable OpenAPI server URL
2025-09-09 12:12:41 +08:00
Tim d42d38ff7a Add configurable OpenAPI server URL 2025-09-09 12:12:10 +08:00
Tim 2b4601bd4b Update CONTRIBUTING.md 2025-09-09 11:56:15 +08:00
Tim 5071d9c6d5 Merge pull request #944 from nagisa77/codex/fix-api-docs-base-url-to-use-https
docs: use https for OpenAPI base URL
2025-09-09 11:48:53 +08:00
Tim cfaa4cd094 Update application.properties 2025-09-09 11:48:42 +08:00
Tim fc414794ff docs: use https for openapi base url 2025-09-09 11:48:07 +08:00
Tim d8264956c3 Merge pull request #943 from nagisa77/codex/fix-invalid-workflow-permissions-in-deploy-staging.yml
fix: grant write permissions for docs deployment
2025-09-09 11:30:28 +08:00
Tim effa7f25ca fix: grant write permissions for docs deployment 2025-09-09 11:30:11 +08:00
Tim 9b19fae69a Merge pull request #942 from nagisa77/codex/resolve-conflict-between-deploy-staging-and-deploy-docs
Run docs deployment after staging deploy
2025-09-09 11:06:39 +08:00
Tim ec04f64ce1 chore: trigger docs deployment after staging 2025-09-09 11:06:16 +08:00
Tim 50bea76c0e Merge pull request #940 from nagisa77/codex/adjust-diff2html-font-for-mobile-ui
style: adjust diff2html fonts on mobile
2025-09-09 00:33:58 +08:00
tim 05522fcdc7 fix: 修改分割线颜色 2025-09-09 00:32:17 +08:00
tim 3820eaa774 fix: changlog--移动端支持换行 #938 2025-09-09 00:23:53 +08:00
Tim 7effaf920a style: adjust diff2html fonts on mobile 2025-09-08 23:48:32 +08:00
Tim e40a6a3ca9 Merge pull request #935 from smallclover/main
redis功能-注册找回密码
2025-09-08 17:14:04 +08:00
Tim 7c9475cfe2 Merge pull request #936 from nagisa77/codex/fix-compilation-issues-in-postservicetest
test: add PostChangeLogService to PostService tests
2025-09-08 15:42:20 +08:00
Tim 17929dd95d test: add PostChangeLogService to PostService tests 2025-09-08 15:42:08 +08:00
Tim f478b55538 Merge pull request #924 from nagisa77/codex/add-article-metadata-change-logging
Track post metadata changes and display in timeline
2025-09-08 15:35:44 +08:00
wangshun 43fa408f46 redis功能-注册找回密码
+ 注册功能,验证码使用缓存,五分钟过期
+ 重置密码,验证码使用缓存,五分钟过期
2025-09-08 15:23:52 +08:00
23 changed files with 201 additions and 85 deletions
+8 -1
View File
@@ -1,7 +1,11 @@
name: Deploy Documentation name: Deploy Documentation
on: on:
push: workflow_call:
inputs:
build-id:
required: false
type: string
workflow_dispatch: workflow_dispatch:
permissions: permissions:
@@ -16,6 +20,9 @@ jobs:
with: with:
fetch-depth: 1 fetch-depth: 1
- name: Log build
run: echo "Running documentation deployment from build ${{ inputs.build-id }}"
- name: Setup Bun - name: Setup Bun
uses: oven-sh/setup-bun@v1 uses: oven-sh/setup-bun@v1
with: with:
+11
View File
@@ -5,6 +5,9 @@ on:
branches: [main] branches: [main]
workflow_dispatch: workflow_dispatch:
permissions:
contents: write
jobs: jobs:
build-and-deploy: build-and-deploy:
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -21,3 +24,11 @@ jobs:
key: ${{ secrets.SSH_KEY }} key: ${{ secrets.SSH_KEY }}
script: bash /opt/openisle/deploy-staging.sh script: bash /opt/openisle/deploy-staging.sh
deploy-docs:
needs: build-and-deploy
if: ${{ success() }}
uses: ./.github/workflows/deploy-docs.yml
secrets: inherit
with:
build-id: ${{ github.run_id }}
+6
View File
@@ -246,3 +246,9 @@ https://resend.com/emails 创建账号并登录
`RESEND_FROM_EMAIL` **noreply@域名** `RESEND_FROM_EMAIL` **noreply@域名**
`RESEND_API_KEY`**刚刚复制的 Key** `RESEND_API_KEY`**刚刚复制的 Key**
![image-20250906151218330](assets/contributing/image-20250906151218330.png) ![image-20250906151218330](assets/contributing/image-20250906151218330.png)
## 开源共建和API文档
- API文档: https://openisle-docs.netlify.app/docs/openapi
+2
View File
@@ -4,6 +4,8 @@
高效的开源社区前后端平台 高效的开源社区前后端平台
<br><br><br> <br><br><br>
<img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200"> <img alt="Image" src="https://openisle-1307107697.cos.accelerate.myqcloud.com/dynamic_assert/22752cfac5a04a9c90c41995b9f55fed.png" width="1200">
<br><br><br>
<a href="https://hellogithub.com/repository/nagisa77/OpenIsle" target="_blank"><img src="https://abroad.hellogithub.com/v1/widgets/recommend.svg?rid=8605546658d94cbab45182af2a02e4c8&claim_uid=p5GNFTtZl6HBAYQ" alt="FeaturedHelloGitHub" style="width: 250px; height: 54px;" width="250" height="54" /></a>
</p> </p>
## 💡 简介 ## 💡 简介
@@ -40,6 +40,8 @@ public class CachingConfig {
public static final String CATEGORY_CACHE_NAME="openisle_categories"; public static final String CATEGORY_CACHE_NAME="openisle_categories";
// 在线人数缓存名 // 在线人数缓存名
public static final String ONLINE_CACHE_NAME="openisle_online"; public static final String ONLINE_CACHE_NAME="openisle_online";
// 注册验证码
public static final String VERIFY_CACHE_NAME="openisle_verify";
/** /**
* 自定义Redis的序列化器 * 自定义Redis的序列化器
@@ -5,6 +5,9 @@ import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.Info;
import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityRequirement;
import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.security.SecurityScheme;
import io.swagger.v3.oas.models.servers.Server;
import java.util.List;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -27,6 +30,9 @@ public class OpenApiConfig {
@Value("${springdoc.info.header}") @Value("${springdoc.info.header}")
private String header; private String header;
@Value("${springdoc.api-docs.server-url}")
private String serverUrl;
@Bean @Bean
public OpenAPI openAPI() { public OpenAPI openAPI() {
SecurityScheme securityScheme = new SecurityScheme() SecurityScheme securityScheme = new SecurityScheme()
@@ -37,12 +43,12 @@ public class OpenApiConfig {
.name(header); .name(header);
return new OpenAPI() return new OpenAPI()
.servers(List.of(new Server().url(serverUrl)))
.info(new Info() .info(new Info()
.title(title) .title(title)
.description(description) .description(description)
.version(version)) .version(version))
.components(new Components() .components(new Components().addSecuritySchemes("JWT", securityScheme))
.addSecuritySchemes("JWT", securityScheme))
.addSecurityItem(new SecurityRequirement().addList("JWT")); .addSecurityItem(new SecurityRequirement().addList("JWT"));
} }
} }
@@ -1,18 +1,22 @@
package com.openisle.controller; package com.openisle.controller;
import com.openisle.config.CachingConfig;
import com.openisle.dto.*; import com.openisle.dto.*;
import com.openisle.exception.FieldException; import com.openisle.exception.FieldException;
import com.openisle.model.RegisterMode; import com.openisle.model.RegisterMode;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.service.*; import com.openisle.service.*;
import com.openisle.util.VerifyType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.TimeUnit;
@RestController @RestController
@RequestMapping("/api/auth") @RequestMapping("/api/auth")
@@ -56,7 +60,8 @@ public class AuthController {
User user = userService.registerWithInvite( User user = userService.registerWithInvite(
req.getUsername(), req.getEmail(), req.getPassword()); req.getUsername(), req.getEmail(), req.getPassword());
inviteService.consume(req.getInviteToken(), user.getUsername()); inviteService.consume(req.getInviteToken(), user.getUsername());
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); // 发送确认邮件
userService.sendVerifyMail(user, VerifyType.REGISTER);
return ResponseEntity.ok(Map.of( return ResponseEntity.ok(Map.of(
"token", jwtService.generateToken(user.getUsername()), "token", jwtService.generateToken(user.getUsername()),
"reason_code", "INVITE_APPROVED" "reason_code", "INVITE_APPROVED"
@@ -70,7 +75,8 @@ public class AuthController {
} }
User user = userService.register( User user = userService.register(
req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode()); req.getUsername(), req.getEmail(), req.getPassword(), "", registerModeService.getRegisterMode());
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); // 发送确认邮件
userService.sendVerifyMail(user, VerifyType.REGISTER);
if (!user.isApproved()) { if (!user.isApproved()) {
notificationService.createRegisterRequestNotifications(user, user.getRegisterReason()); notificationService.createRegisterRequestNotifications(user, user.getRegisterReason());
} }
@@ -79,13 +85,12 @@ public class AuthController {
@PostMapping("/verify") @PostMapping("/verify")
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) { public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
boolean ok = userService.verifyCode(req.getUsername(), req.getCode()); Optional<User> userOpt = userService.findByUsername(req.getUsername());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
}
boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.REGISTER);
if (ok) { if (ok) {
Optional<User> userOpt = userService.findByUsername(req.getUsername());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
}
User user = userOpt.get(); User user = userOpt.get();
if (user.isApproved()) { if (user.isApproved()) {
@@ -122,7 +127,7 @@ public class AuthController {
User user = userOpt.get(); User user = userOpt.get();
if (!user.isVerified()) { if (!user.isVerified()) {
user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode()); user = userService.register(user.getUsername(), user.getEmail(), user.getPassword(), user.getRegisterReason(), registerModeService.getRegisterMode());
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode()); userService.sendVerifyMail(user, VerifyType.REGISTER);
return ResponseEntity.badRequest().body(Map.of( return ResponseEntity.badRequest().body(Map.of(
"error", "User not verified", "error", "User not verified",
"reason_code", "NOT_VERIFIED", "reason_code", "NOT_VERIFIED",
@@ -417,14 +422,17 @@ public class AuthController {
if (userOpt.isEmpty()) { if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "User not found")); return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
} }
String code = userService.generatePasswordResetCode(req.getEmail()); userService.sendVerifyMail(userOpt.get(), VerifyType.RESET_PASSWORD);
emailService.sendEmail(req.getEmail(), "请填写验证码以重置密码", "您的验证码是" + code);
return ResponseEntity.ok(Map.of("message", "Verification code sent")); return ResponseEntity.ok(Map.of("message", "Verification code sent"));
} }
@PostMapping("/forgot/verify") @PostMapping("/forgot/verify")
public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) { public ResponseEntity<?> verifyReset(@RequestBody VerifyForgotRequest req) {
boolean ok = userService.verifyPasswordResetCode(req.getEmail(), req.getCode()); Optional<User> userOpt = userService.findByEmail(req.getEmail());
if (userOpt.isEmpty()) {
return ResponseEntity.badRequest().body(Map.of("error", "User not found"));
}
boolean ok = userService.verifyCode(userOpt.get(), req.getCode(), VerifyType.RESET_PASSWORD);
if (ok) { if (ok) {
String username = userService.findByEmail(req.getEmail()).get().getUsername(); String username = userService.findByEmail(req.getEmail()).get().getUsername();
return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username))); return ResponseEntity.ok(Map.of("token", jwtService.generateResetToken(username)));
@@ -1,5 +1,6 @@
package com.openisle.service; package com.openisle.service;
import com.openisle.config.CachingConfig;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.model.Role; import com.openisle.model.Role;
import com.openisle.service.PasswordValidator; import com.openisle.service.PasswordValidator;
@@ -7,13 +8,18 @@ import com.openisle.service.UsernameValidator;
import com.openisle.service.AvatarGenerator; import com.openisle.service.AvatarGenerator;
import com.openisle.exception.FieldException; import com.openisle.exception.FieldException;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.util.VerifyType;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Random; import java.util.Random;
import java.util.concurrent.TimeUnit;
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
@@ -25,6 +31,10 @@ public class UserService {
private final ImageUploader imageUploader; private final ImageUploader imageUploader;
private final AvatarGenerator avatarGenerator; private final AvatarGenerator avatarGenerator;
private final RedisTemplate redisTemplate;
private final EmailSender emailService;
public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) { public User register(String username, String email, String password, String reason, com.openisle.model.RegisterMode mode) {
usernameValidator.validate(username); usernameValidator.validate(username);
passwordValidator.validate(password); passwordValidator.validate(password);
@@ -38,7 +48,7 @@ public class UserService {
// 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码 // 未验证 → 允许“重注册”:覆盖必要字段并重新发验证码
u.setEmail(email); // 若不允许改邮箱可去掉 u.setEmail(email); // 若不允许改邮箱可去掉
u.setPassword(passwordEncoder.encode(password)); u.setPassword(passwordEncoder.encode(password));
u.setVerificationCode(genCode()); // u.setVerificationCode(genCode());
u.setRegisterReason(reason); u.setRegisterReason(reason);
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(u); return userRepository.save(u);
@@ -54,7 +64,7 @@ public class UserService {
// 未验证 → 允许“重注册” // 未验证 → 允许“重注册”
u.setUsername(username); // 若不允许改用户名可去掉 u.setUsername(username); // 若不允许改用户名可去掉
u.setPassword(passwordEncoder.encode(password)); u.setPassword(passwordEncoder.encode(password));
u.setVerificationCode(genCode()); // u.setVerificationCode(genCode());
u.setRegisterReason(reason); u.setRegisterReason(reason);
u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); u.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(u); return userRepository.save(u);
@@ -67,7 +77,7 @@ public class UserService {
user.setPassword(passwordEncoder.encode(password)); user.setPassword(passwordEncoder.encode(password));
user.setRole(Role.USER); user.setRole(Role.USER);
user.setVerified(false); user.setVerified(false);
user.setVerificationCode(genCode()); // user.setVerificationCode(genCode());
user.setAvatar(avatarGenerator.generate(username)); user.setAvatar(avatarGenerator.generate(username));
user.setRegisterReason(reason); user.setRegisterReason(reason);
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT); user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
@@ -77,7 +87,7 @@ public class UserService {
public User registerWithInvite(String username, String email, String password) { public User registerWithInvite(String username, String email, String password) {
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT); User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
user.setVerified(true); user.setVerified(true);
user.setVerificationCode(genCode()); // user.setVerificationCode(genCode());
return userRepository.save(user); return userRepository.save(user);
} }
@@ -85,16 +95,58 @@ public class UserService {
return String.format("%06d", new Random().nextInt(1000000)); return String.format("%06d", new Random().nextInt(1000000));
} }
public boolean verifyCode(String username, String code) { /**
Optional<User> userOpt = userRepository.findByUsername(username); * 将验证码存入缓存,并发送邮件
if (userOpt.isPresent() && code.equals(userOpt.get().getVerificationCode())) { * @param user
User user = userOpt.get(); */
user.setVerified(true); public void sendVerifyMail(User user, VerifyType verifyType){
user.setVerificationCode(null); //缓存验证码
userRepository.save(user); String code = genCode();
return true; String key;
String subject;
String content = "您的验证码是:" + code;
// 注册类型
if(verifyType.equals(VerifyType.REGISTER)){
key = CachingConfig.VERIFY_CACHE_NAME + ":register:code:" + user.getUsername();
subject = "在网站填写验证码以验证(有效期为5分钟)";
}else {
// 重置密码
key = CachingConfig.VERIFY_CACHE_NAME + ":reset_password:code:" + user.getUsername();
subject = "请填写验证码以重置密码(有效期为5分钟)";
} }
return false;
redisTemplate.opsForValue().set(key, code, 5, TimeUnit.MINUTES);// 五分钟后验证码过期
emailService.sendEmail(user.getEmail(), subject, content);
}
/**
* 验证code是否正确
* @param user
* @param code
* @param verifyType
* @return
*/
public boolean verifyCode(User user, String code, VerifyType verifyType) {
// 生成key
String key1 = VerifyType.REGISTER.equals(verifyType)?":register:code:":":reset_password:code:";
String key = CachingConfig.VERIFY_CACHE_NAME + key1 + user.getUsername();
// 这里不能使用getAndDelete,需要6.x版本
String cachedCode = (String)redisTemplate.opsForValue().get(key);
// 如果校验code过期或者不存在
// 或者校验code不一致
if(Objects.isNull(cachedCode)
|| !cachedCode.equals(code)){
return false;
}
// 注册模式需要设置已经确认
if(VerifyType.REGISTER.equals(verifyType)){
user.setVerified(true);
userRepository.save(user);
}
// 走到这里说明验证成功删除验证码
redisTemplate.delete(key);
return true;
} }
public Optional<User> authenticate(String username, String password) { public Optional<User> authenticate(String username, String password) {
@@ -165,26 +217,6 @@ public class UserService {
return userRepository.save(user); return userRepository.save(user);
} }
public String generatePasswordResetCode(String email) {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
String code = genCode();
user.setPasswordResetCode(code);
userRepository.save(user);
return code;
}
public boolean verifyPasswordResetCode(String email, String code) {
Optional<User> userOpt = userRepository.findByEmail(email);
if (userOpt.isPresent() && code.equals(userOpt.get().getPasswordResetCode())) {
User user = userOpt.get();
user.setPasswordResetCode(null);
userRepository.save(user);
return true;
}
return false;
}
public User updatePassword(String username, String newPassword) { public User updatePassword(String username, String newPassword) {
passwordValidator.validate(newPassword); passwordValidator.validate(newPassword);
User user = userRepository.findByUsername(username) User user = userRepository.findByUsername(username)
@@ -0,0 +1,20 @@
package com.openisle.util;
/**
* 验证码类型
* @author smallclover
* @since 2025-09-08
*/
public enum VerifyType {
REGISTER(1),
RESET_PASSWORD(2);
private final int code;
VerifyType(int code) {
this.code = code;
}
public int getCode() {
return code;
}
}
@@ -108,6 +108,7 @@ rabbitmq.sharding.enabled=true
# see https://springdoc.org/#springdoc-openapi-core-properties # see https://springdoc.org/#springdoc-openapi-core-properties
springdoc.api-docs.path=/api/v3/api-docs springdoc.api-docs.path=/api/v3/api-docs
springdoc.api-docs.enabled=true springdoc.api-docs.enabled=true
springdoc.api-docs.server-url=${WEBSITE_URL:https://www.open-isle.com}
springdoc.info.title=OpenIsle springdoc.info.title=OpenIsle
springdoc.info.description=OpenIsle Open API Documentation springdoc.info.description=OpenIsle Open API Documentation
springdoc.info.version=0.0.1 springdoc.info.version=0.0.1
@@ -4,6 +4,7 @@ import com.openisle.model.User;
import com.openisle.service.*; import com.openisle.service.*;
import com.openisle.model.RegisterMode; import com.openisle.model.RegisterMode;
import com.openisle.repository.UserRepository; import com.openisle.repository.UserRepository;
import com.openisle.util.VerifyType;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.mockito.Mockito; import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -71,7 +72,9 @@ class AuthControllerTest {
@Test @Test
void verifyCodeEndpoint() throws Exception { void verifyCodeEndpoint() throws Exception {
Mockito.when(userService.verifyCode("u", "123")).thenReturn(true); User user = new User();
user.setUsername("u");
Mockito.when(userService.verifyCode(user, "123", VerifyType.REGISTER)).thenReturn(true);
Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token"); Mockito.when(jwtService.generateReasonToken("u")).thenReturn("reason_token");
mockMvc.perform(post("/api/auth/verify") mockMvc.perform(post("/api/auth/verify")
@@ -37,11 +37,12 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -86,11 +87,12 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
Post post = new Post(); Post post = new Post();
@@ -141,11 +143,12 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L); when(postRepo.countByAuthorAfter(eq("alice"), any())).thenReturn(1L);
@@ -177,11 +180,12 @@ class PostServiceTest {
EmailSender emailSender = mock(EmailSender.class); EmailSender emailSender = mock(EmailSender.class);
ApplicationContext context = mock(ApplicationContext.class); ApplicationContext context = mock(ApplicationContext.class);
PointService pointService = mock(PointService.class); PointService pointService = mock(PointService.class);
PostChangeLogService postChangeLogService = mock(PostChangeLogService.class);
PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo, PostService service = new PostService(postRepo, userRepo, catRepo, tagRepo, lotteryRepo,
pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo, pollPostRepo, pollVoteRepo, notifService, subService, commentService, commentRepo,
reactionRepo, subRepo, notificationRepo, postReadService, reactionRepo, subRepo, notificationRepo, postReadService,
imageUploader, taskScheduler, emailSender, context, pointService, PublishMode.DIRECT); imageUploader, taskScheduler, emailSender, context, pointService, postChangeLogService, PublishMode.DIRECT);
when(context.getBean(PostService.class)).thenReturn(service); when(context.getBean(PostService.class)).thenReturn(service);
User author = new User(); User author = new User();
+3 -3
View File
@@ -16,6 +16,6 @@ bun dev
使用以下路由: 使用以下路由:
- `docs/frontend/` 前端技术文档 - `frontend/` 前端技术文档
- `docs/backend/` 后端技术文档 - `backend/` 后端技术文档
- `docs/openapi/` 后端 API 文档 - `openapi/` 后端 API 文档
@@ -19,7 +19,7 @@ function DocsCategory({ url }: { url: string }) {
); );
} }
export default async function Page(props: PageProps<'/docs/[[...slug]]'>) { export default async function Page(props: PageProps<'/[[...slug]]'>) {
const params = await props.params; const params = await props.params;
const page = source.getPage(params.slug); const page = source.getPage(params.slug);
if (!page) notFound(); if (!page) notFound();
@@ -48,7 +48,7 @@ export async function generateStaticParams() {
} }
export async function generateMetadata( export async function generateMetadata(
props: PageProps<'/docs/[[...slug]]'> props: PageProps<'/[[...slug]]'>
): Promise<Metadata> { ): Promise<Metadata> {
const params = await props.params; const params = await props.params;
const page = source.getPage(params.slug); const page = source.getPage(params.slug);
@@ -28,7 +28,7 @@ function TabTitle({ children }: { children: React.ReactNode }) {
return <span className="text-[11px]">{children}</span>; return <span className="text-[11px]">{children}</span>;
} }
export default function Layout({ children }: LayoutProps<'/docs'>) { export default function Layout({ children }: LayoutProps<'/'>) {
return ( return (
// @ts-ignore // @ts-ignore
<DocsLayout <DocsLayout
@@ -40,7 +40,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
{ {
title: 'OpenIsle 前端', title: 'OpenIsle 前端',
description: <TabTitle></TabTitle>, description: <TabTitle></TabTitle>,
url: '/docs/frontend', url: '/frontend',
icon: ( icon: (
<TabIcon color="#4ca154"> <TabIcon color="#4ca154">
<CompassIcon /> <CompassIcon />
@@ -50,7 +50,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
{ {
title: 'OpenIsle 后端', title: 'OpenIsle 后端',
description: <TabTitle></TabTitle>, description: <TabTitle></TabTitle>,
url: '/docs/backend', url: '/backend',
icon: ( icon: (
<TabIcon color="#1f66f4"> <TabIcon color="#1f66f4">
<ServerIcon /> <ServerIcon />
@@ -60,7 +60,7 @@ export default function Layout({ children }: LayoutProps<'/docs'>) {
{ {
title: 'OpenIsle API', title: 'OpenIsle API',
description: <TabTitle> API </TabTitle>, description: <TabTitle> API </TabTitle>,
url: '/docs/openapi', url: '/openapi',
icon: ( icon: (
<TabIcon color="#677489"> <TabIcon color="#677489">
<CodeXmlIcon /> <CodeXmlIcon />
+1 -1
View File
@@ -6,7 +6,7 @@ const inter = Inter({
subsets: ['latin'], subsets: ['latin'],
}); });
export default function Layout({ children }: LayoutProps<'/docs'>) { export default function Layout({ children }: LayoutProps<'/'>) {
return ( return (
<html lang="zh" className={inter.className} suppressHydrationWarning> <html lang="zh" className={inter.className} suppressHydrationWarning>
<body className="flex flex-col min-h-screen"> <body className="flex flex-col min-h-screen">
+1 -1
View File
@@ -40,4 +40,4 @@ backend/
## API 接口 ## API 接口
详细的 API 接口文档请查看 [API 文档](/docs/openapi)。 详细的 API 接口文档请查看 [API 文档](/openapi)。
+3 -3
View File
@@ -9,6 +9,6 @@ OpenIsle 是一个现代化的社区平台,提供完整的社交功能。
## 快速开始 ## 快速开始
- [后端开发指南](/docs/backend) - 了解后端架构和开发 - [后端开发指南](/backend) - 了解后端架构和开发
- [前端开发指南](/docs/frontend) - 了解前端技术栈和组件 - [前端开发指南](/frontend) - 了解前端技术栈和组件
- [API 文档](/docs/openapi) - 查看完整的 API 接口文档 - [API 文档](/openapi) - 查看完整的 API 接口文档
+1 -1
View File
@@ -8,7 +8,7 @@ export function baseOptions(): BaseLayoutProps {
githubUrl: 'https://github.com/nagisa77/OpenIsle', githubUrl: 'https://github.com/nagisa77/OpenIsle',
nav: { nav: {
title: 'OpenIsle Docs', title: 'OpenIsle Docs',
url: '/docs', url: '/',
}, },
searchToggle: { searchToggle: {
enabled: false, enabled: false,
+1 -1
View File
@@ -10,7 +10,7 @@ import * as ClientAdapters from './media-adapter.client';
// See https://fumadocs.vercel.app/docs/headless/source-api for more info // See https://fumadocs.vercel.app/docs/headless/source-api for more info
export const source = loader({ export const source = loader({
// it assigns a URL to your pages // it assigns a URL to your pages
baseUrl: '/docs', baseUrl: '/',
source: docs.toFumadocsSource(), source: docs.toFumadocsSource(),
pageTree: { pageTree: {
transformers: [transformerOpenAPI()], transformers: [transformerOpenAPI()],
+17 -1
View File
@@ -17,7 +17,7 @@
--background-color: white; --background-color: white;
--background-color-blur: rgba(255, 255, 255, 0.57); --background-color-blur: rgba(255, 255, 255, 0.57);
--menu-border-color: lightgray; --menu-border-color: lightgray;
--normal-border-color: lightgray; --normal-border-color: rgba(211, 211, 211, 0.63);
--menu-selected-background-color: rgba(88, 241, 255, 0.166); --menu-selected-background-color: rgba(88, 241, 255, 0.166);
--normal-light-background-color: rgba(242, 242, 242, 0.884); --normal-light-background-color: rgba(242, 242, 242, 0.884);
--menu-selected-background-color-hover: rgba(242, 242, 242, 0.884); --menu-selected-background-color-hover: rgba(242, 242, 242, 0.884);
@@ -348,6 +348,22 @@ body {
} }
} }
/* Adjust diff2html layout on mobile */
@media (max-width: 768px) {
.content-diff .d2h-wrapper,
.content-diff .d2h-code-line,
.content-diff .d2h-code-side-line,
.content-diff .d2h-code-line-ctn,
.content-diff .d2h-code-side-line-ctn,
.content-diff .d2h-file-header {
font-size: 12px;
}
.content-diff .d2h-wrapper {
overflow-x: auto;
}
}
/* Transition API */ /* Transition API */
::view-transition-old(root), ::view-transition-old(root),
::view-transition-new(root) { ::view-transition-new(root) {
@@ -11,18 +11,18 @@
<span v-if="log.username" class="change-log-user">{{ log.username }}</span> <span v-if="log.username" class="change-log-user">{{ log.username }}</span>
<span v-if="log.type === 'CONTENT'" class="change-log-content">变更了文章内容</span> <span v-if="log.type === 'CONTENT'" class="change-log-content">变更了文章内容</span>
<span v-else-if="log.type === 'TITLE'" class="change-log-content">变更了文章标题</span> <span v-else-if="log.type === 'TITLE'" class="change-log-content">变更了文章标题</span>
<span v-else-if="log.type === 'CATEGORY'" class="change-log-content change-log-category"> <template v-else-if="log.type === 'CATEGORY'">
<div class="change-log-category-text">变更了文章分类, </div> <div class="change-log-category-text">变更了文章分类, </div>
<ArticleCategory :category="log.oldCategory" /> <ArticleCategory :category="log.oldCategory" />
<div class="change-log-category-text">修改为</div> <div class="change-log-category-text">修改为</div>
<ArticleCategory :category="log.newCategory" /> <ArticleCategory :category="log.newCategory" />
</span> </template>
<span v-else-if="log.type === 'TAG'" class="change-log-content change-log-category"> <template v-else-if="log.type === 'TAG'">
<div class="change-log-category-text">变更了文章标签, </div> <div class="change-log-category-text">变更了文章标签, </div>
<ArticleTags :tags="log.oldTags" /> <ArticleTags :tags="log.oldTags" />
<div class="change-log-category-text">修改为</div> <div class="change-log-category-text">修改为</div>
<ArticleTags :tags="log.newTags" /> <ArticleTags :tags="log.newTags" />
</span> </template>
<span v-else-if="log.type === 'CLOSED'" class="change-log-content"> <span v-else-if="log.type === 'CLOSED'" class="change-log-content">
<template v-if="log.newClosed">关闭了文章</template> <template v-if="log.newClosed">关闭了文章</template>
<template v-else>重新打开了文章</template> <template v-else>重新打开了文章</template>
@@ -68,7 +68,6 @@ const props = defineProps({
}) })
const diffHtml = computed(() => { const diffHtml = computed(() => {
const isMobile = useIsMobile()
// Track theme changes // Track theme changes
const isDark = import.meta.client && document.documentElement.dataset.theme === 'dark' const isDark = import.meta.client && document.documentElement.dataset.theme === 'dark'
themeState.mode themeState.mode
@@ -83,7 +82,6 @@ const diffHtml = computed(() => {
showFiles: false, showFiles: false,
matching: 'lines', matching: 'lines',
drawFileList: false, drawFileList: false,
outputFormat: isMobile.value ? 'line-by-line' : 'side-by-side',
colorScheme, colorScheme,
}) })
} else if (props.log.type === 'TITLE') { } else if (props.log.type === 'TITLE') {
@@ -95,7 +93,6 @@ const diffHtml = computed(() => {
showFiles: false, showFiles: false,
matching: 'lines', matching: 'lines',
drawFileList: false, drawFileList: false,
outputFormat: isMobile.value ? 'line-by-line' : 'side-by-side',
colorScheme, colorScheme,
}) })
} }
@@ -110,9 +107,12 @@ const diffHtml = computed(() => {
/* padding-top: 5px; */ /* padding-top: 5px; */
/* padding-bottom: 30px; */ /* padding-bottom: 30px; */
font-size: 14px; font-size: 14px;
border-bottom: 1px solid var(--normal-border-color);
padding-bottom: 10px;
} }
.change-log-text { .change-log-text {
display: flex; display: flex;
flex-wrap: wrap;
align-items: center; align-items: center;
} }
.change-log-user { .change-log-user {
@@ -146,5 +146,6 @@ const diffHtml = computed(() => {
flex-direction: row; flex-direction: row;
gap: 4px; gap: 4px;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
</style> </style>
+2 -5
View File
@@ -1163,6 +1163,7 @@ onMounted(async () => {
margin-top: 10px; margin-top: 10px;
gap: 10px; gap: 10px;
align-items: center; align-items: center;
flex-wrap: wrap;
} }
.info-content-container { .info-content-container {
@@ -1218,7 +1219,7 @@ onMounted(async () => {
} }
.post-time { .post-time {
font-size: 14px; font-size: 12px;
opacity: 0.5; opacity: 0.5;
} }
@@ -1284,10 +1285,6 @@ onMounted(async () => {
font-size: 12px; font-size: 12px;
} }
.post-time {
font-size: 12px;
}
.info-content-text { .info-content-text {
line-height: 1.5; line-height: 1.5;
} }