Merge pull request #206 from nagisa77/codex/add-whitelist-invitation-registration-mode

Implement whitelist registration mode
This commit is contained in:
Tim
2025-07-14 21:32:23 +08:00
committed by GitHub
21 changed files with 161 additions and 36 deletions

View File

@@ -5,6 +5,8 @@ import com.openisle.model.PublishMode;
import com.openisle.service.PasswordValidator; import com.openisle.service.PasswordValidator;
import com.openisle.service.PostService; import com.openisle.service.PostService;
import com.openisle.service.AiUsageService; import com.openisle.service.AiUsageService;
import com.openisle.service.RegisterModeService;
import com.openisle.model.RegisterMode;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
@@ -16,6 +18,7 @@ public class AdminConfigController {
private final PostService postService; private final PostService postService;
private final PasswordValidator passwordValidator; private final PasswordValidator passwordValidator;
private final AiUsageService aiUsageService; private final AiUsageService aiUsageService;
private final RegisterModeService registerModeService;
@GetMapping @GetMapping
public ConfigDto getConfig() { public ConfigDto getConfig() {
@@ -23,6 +26,7 @@ public class AdminConfigController {
dto.setPublishMode(postService.getPublishMode()); dto.setPublishMode(postService.getPublishMode());
dto.setPasswordStrength(passwordValidator.getStrength()); dto.setPasswordStrength(passwordValidator.getStrength());
dto.setAiFormatLimit(aiUsageService.getFormatLimit()); dto.setAiFormatLimit(aiUsageService.getFormatLimit());
dto.setRegisterMode(registerModeService.getRegisterMode());
return dto; return dto;
} }
@@ -37,6 +41,9 @@ public class AdminConfigController {
if (dto.getAiFormatLimit() != null) { if (dto.getAiFormatLimit() != null) {
aiUsageService.setFormatLimit(dto.getAiFormatLimit()); aiUsageService.setFormatLimit(dto.getAiFormatLimit());
} }
if (dto.getRegisterMode() != null) {
registerModeService.setRegisterMode(dto.getRegisterMode());
}
return getConfig(); return getConfig();
} }
@@ -45,5 +52,6 @@ public class AdminConfigController {
private PublishMode publishMode; private PublishMode publishMode;
private PasswordStrength passwordStrength; private PasswordStrength passwordStrength;
private Integer aiFormatLimit; private Integer aiFormatLimit;
private RegisterMode registerMode;
} }
} }

View File

@@ -0,0 +1,36 @@
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.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;
@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(), "Registration Approved",
"Your account has been approved. Visit: https://www.open-isle.com");
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(), "Registration Rejected",
"Your account request was rejected. Visit: https://www.open-isle.com");
return ResponseEntity.ok().build();
}
}

View File

@@ -6,6 +6,8 @@ import com.openisle.service.JwtService;
import com.openisle.service.UserService; import com.openisle.service.UserService;
import com.openisle.service.CaptchaService; import com.openisle.service.CaptchaService;
import com.openisle.service.GoogleAuthService; import com.openisle.service.GoogleAuthService;
import com.openisle.service.RegisterModeService;
import com.openisle.model.RegisterMode;
import lombok.Data; import lombok.Data;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
@@ -23,6 +25,7 @@ public class AuthController {
private final EmailSender emailService; private final EmailSender emailService;
private final CaptchaService captchaService; private final CaptchaService captchaService;
private final GoogleAuthService googleAuthService; private final GoogleAuthService googleAuthService;
private final RegisterModeService registerModeService;
@Value("${app.captcha.enabled:false}") @Value("${app.captcha.enabled:false}")
private boolean captchaEnabled; private boolean captchaEnabled;
@@ -38,7 +41,8 @@ public class AuthController {
if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) { if (captchaEnabled && registerCaptchaEnabled && !captchaService.verify(req.getCaptcha())) {
return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha")); return ResponseEntity.badRequest().body(Map.of("error", "Invalid captcha"));
} }
User user = userService.register(req.getUsername(), req.getEmail(), req.getPassword()); User user = userService.register(
req.getUsername(), req.getEmail(), req.getPassword(), req.getReason(), registerModeService.getRegisterMode());
emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode()); emailService.sendEmail(user.getEmail(), "Verification Code", "Your verification code is " + user.getVerificationCode());
return ResponseEntity.ok(Map.of("message", "Verification code sent")); return ResponseEntity.ok(Map.of("message", "Verification code sent"));
} }
@@ -67,8 +71,11 @@ public class AuthController {
@PostMapping("/google") @PostMapping("/google")
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) { public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
Optional<User> user = googleAuthService.authenticate(req.getIdToken()); Optional<User> user = googleAuthService.authenticate(req.getIdToken(), req.getReason(), registerModeService.getRegisterMode());
if (user.isPresent()) { if (user.isPresent()) {
if (!user.get().isApproved()) {
return ResponseEntity.badRequest().body(Map.of("error", "Account awaiting approval"));
}
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername()))); return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
} }
return ResponseEntity.badRequest().body(Map.of("error", "Invalid google token")); return ResponseEntity.badRequest().body(Map.of("error", "Invalid google token"));
@@ -85,6 +92,7 @@ public class AuthController {
private String email; private String email;
private String password; private String password;
private String captcha; private String captcha;
private String reason;
} }
@Data @Data
@@ -97,6 +105,7 @@ public class AuthController {
@Data @Data
private static class GoogleLoginRequest { private static class GoogleLoginRequest {
private String idToken; private String idToken;
private String reason;
} }
@Data @Data

View File

@@ -2,12 +2,15 @@ package com.openisle.controller;
import lombok.Data; import lombok.Data;
import org.springframework.beans.factory.annotation.Value; 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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping("/api") @RequestMapping("/api")
@lombok.RequiredArgsConstructor
public class ConfigController { public class ConfigController {
@Value("${app.captcha.enabled:false}") @Value("${app.captcha.enabled:false}")
@@ -28,6 +31,8 @@ public class ConfigController {
@Value("${app.ai.format-limit:3}") @Value("${app.ai.format-limit:3}")
private int aiFormatLimit; private int aiFormatLimit;
private final RegisterModeService registerModeService;
@GetMapping("/config") @GetMapping("/config")
public ConfigResponse getConfig() { public ConfigResponse getConfig() {
ConfigResponse resp = new ConfigResponse(); ConfigResponse resp = new ConfigResponse();
@@ -37,6 +42,7 @@ public class ConfigController {
resp.setPostCaptchaEnabled(postCaptchaEnabled); resp.setPostCaptchaEnabled(postCaptchaEnabled);
resp.setCommentCaptchaEnabled(commentCaptchaEnabled); resp.setCommentCaptchaEnabled(commentCaptchaEnabled);
resp.setAiFormatLimit(aiFormatLimit); resp.setAiFormatLimit(aiFormatLimit);
resp.setRegisterMode(registerModeService.getRegisterMode());
return resp; return resp;
} }
@@ -48,5 +54,6 @@ public class ConfigController {
private boolean postCaptchaEnabled; private boolean postCaptchaEnabled;
private boolean commentCaptchaEnabled; private boolean commentCaptchaEnabled;
private int aiFormatLimit; private int aiFormatLimit;
private RegisterMode registerMode;
} }
} }

View File

@@ -45,6 +45,9 @@ public class Notification {
@Column(name = "reaction_type") @Column(name = "reaction_type")
private ReactionType reactionType; private ReactionType reactionType;
@Column(length = 1000)
private String content;
@Column @Column
private Boolean approved; private Boolean approved;

View File

@@ -27,5 +27,7 @@ public enum NotificationType {
/** Someone unfollowed you */ /** Someone unfollowed you */
USER_UNFOLLOWED, USER_UNFOLLOWED,
/** A user you subscribe to created a post or comment */ /** A user you subscribe to created a post or comment */
USER_ACTIVITY USER_ACTIVITY,
/** A user requested registration approval */
REGISTER_REQUEST
} }

View File

@@ -0,0 +1,9 @@
package com.openisle.model;
/**
* Application-wide user registration mode.
*/
public enum RegisterMode {
DIRECT,
WHITELIST
}

View File

@@ -42,6 +42,12 @@ public class User {
@Column(length = 1000) @Column(length = 1000)
private String introduction; private String introduction;
@Column(length = 1000)
private String registerReason;
@Column(nullable = false)
private boolean approved = true;
@Enumerated(EnumType.STRING) @Enumerated(EnumType.STRING)
@Column(nullable = false) @Column(nullable = false)
private Role role = Role.USER; private Role role = Role.USER;

View File

@@ -43,16 +43,16 @@ public class CommentService {
comment.setContent(content); comment.setContent(content);
comment = commentRepository.save(comment); comment = commentRepository.save(comment);
if (!author.getId().equals(post.getAuthor().getId())) { if (!author.getId().equals(post.getAuthor().getId())) {
notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment, null); notificationService.createNotification(post.getAuthor(), NotificationType.COMMENT_REPLY, post, comment, null, null, null, null);
} }
for (User u : subscriptionService.getPostSubscribers(postId)) { for (User u : subscriptionService.getPostSubscribers(postId)) {
if (!u.getId().equals(author.getId())) { if (!u.getId().equals(author.getId())) {
notificationService.createNotification(u, NotificationType.POST_UPDATED, post, comment, null); notificationService.createNotification(u, NotificationType.POST_UPDATED, post, comment, null, null, null, null);
} }
} }
for (User u : subscriptionService.getSubscribers(author.getUsername())) { for (User u : subscriptionService.getSubscribers(author.getUsername())) {
if (!u.getId().equals(author.getId())) { if (!u.getId().equals(author.getId())) {
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, post, comment, null); notificationService.createNotification(u, NotificationType.USER_ACTIVITY, post, comment, null, null, null, null);
} }
} }
return comment; return comment;
@@ -70,21 +70,21 @@ public class CommentService {
comment.setContent(content); comment.setContent(content);
comment = commentRepository.save(comment); comment = commentRepository.save(comment);
if (!author.getId().equals(parent.getAuthor().getId())) { if (!author.getId().equals(parent.getAuthor().getId())) {
notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(), comment, null); notificationService.createNotification(parent.getAuthor(), NotificationType.COMMENT_REPLY, parent.getPost(), comment, null, null, null, null);
} }
for (User u : subscriptionService.getCommentSubscribers(parentId)) { for (User u : subscriptionService.getCommentSubscribers(parentId)) {
if (!u.getId().equals(author.getId())) { if (!u.getId().equals(author.getId())) {
notificationService.createNotification(u, NotificationType.COMMENT_REPLY, parent.getPost(), comment, null); notificationService.createNotification(u, NotificationType.COMMENT_REPLY, parent.getPost(), comment, null, null, null, null);
} }
} }
for (User u : subscriptionService.getPostSubscribers(parent.getPost().getId())) { for (User u : subscriptionService.getPostSubscribers(parent.getPost().getId())) {
if (!u.getId().equals(author.getId())) { if (!u.getId().equals(author.getId())) {
notificationService.createNotification(u, NotificationType.POST_UPDATED, parent.getPost(), comment, null); notificationService.createNotification(u, NotificationType.POST_UPDATED, parent.getPost(), comment, null, null, null, null);
} }
} }
for (User u : subscriptionService.getSubscribers(author.getUsername())) { for (User u : subscriptionService.getSubscribers(author.getUsername())) {
if (!u.getId().equals(author.getId())) { if (!u.getId().equals(author.getId())) {
notificationService.createNotification(u, NotificationType.USER_ACTIVITY, parent.getPost(), comment, null); notificationService.createNotification(u, NotificationType.USER_ACTIVITY, parent.getPost(), comment, null, null, null, null);
} }
} }
return comment; return comment;

View File

@@ -23,7 +23,7 @@ public class GoogleAuthService {
@Value("${google.client-id:}") @Value("${google.client-id:}")
private String clientId; private String clientId;
public Optional<User> authenticate(String idTokenString) { public Optional<User> authenticate(String idTokenString, String reason, com.openisle.model.RegisterMode mode) {
GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory()) GoogleIdTokenVerifier verifier = new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())
.setAudience(Collections.singletonList(clientId)) .setAudience(Collections.singletonList(clientId))
.build(); .build();
@@ -35,13 +35,13 @@ public class GoogleAuthService {
GoogleIdToken.Payload payload = idToken.getPayload(); GoogleIdToken.Payload payload = idToken.getPayload();
String email = payload.getEmail(); String email = payload.getEmail();
String name = (String) payload.get("name"); String name = (String) payload.get("name");
return Optional.of(processUser(email, name)); return Optional.of(processUser(email, name, reason, mode));
} catch (Exception e) { } catch (Exception e) {
return Optional.empty(); return Optional.empty();
} }
} }
private User processUser(String email, String name) { private User processUser(String email, String name, String reason, com.openisle.model.RegisterMode mode) {
Optional<User> existing = userRepository.findByEmail(email); Optional<User> existing = userRepository.findByEmail(email);
if (existing.isPresent()) { if (existing.isPresent()) {
User user = existing.get(); User user = existing.get();
@@ -64,6 +64,8 @@ public class GoogleAuthService {
user.setPassword(""); user.setPassword("");
user.setRole(Role.USER); user.setRole(Role.USER);
user.setVerified(true); user.setVerified(true);
user.setRegisterReason(reason);
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
user.setAvatar("https://github.com/identicons/" + username + ".png"); user.setAvatar("https://github.com/identicons/" + username + ".png");
return userRepository.save(user); return userRepository.save(user);
} }

View File

@@ -16,11 +16,11 @@ public class NotificationService {
private final UserRepository userRepository; private final UserRepository userRepository;
public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved) { public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved) {
return createNotification(user, type, post, comment, approved, null, null); return createNotification(user, type, post, comment, approved, null, null, null);
} }
public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved, public Notification createNotification(User user, NotificationType type, Post post, Comment comment, Boolean approved,
User fromUser, ReactionType reactionType) { User fromUser, ReactionType reactionType, String content) {
Notification n = new Notification(); Notification n = new Notification();
n.setUser(user); n.setUser(user);
n.setType(type); n.setType(type);
@@ -29,6 +29,7 @@ public class NotificationService {
n.setApproved(approved); n.setApproved(approved);
n.setFromUser(fromUser); n.setFromUser(fromUser);
n.setReactionType(reactionType); n.setReactionType(reactionType);
n.setContent(content);
return notificationRepository.save(n); return notificationRepository.save(n);
} }

View File

@@ -109,10 +109,10 @@ public class PostService {
java.util.List<User> admins = userRepository.findByRole(com.openisle.model.Role.ADMIN); java.util.List<User> admins = userRepository.findByRole(com.openisle.model.Role.ADMIN);
for (User admin : admins) { for (User admin : admins) {
notificationService.createNotification(admin, notificationService.createNotification(admin,
NotificationType.POST_REVIEW_REQUEST, post, null, null, author, null); NotificationType.POST_REVIEW_REQUEST, post, null, null, author, null, null);
} }
notificationService.createNotification(author, notificationService.createNotification(author,
NotificationType.POST_REVIEW_REQUEST, post, null, null, null, null); NotificationType.POST_REVIEW_REQUEST, post, null, null, null, null, null);
} }
// notify followers of author // notify followers of author
for (User u : subscriptionService.getSubscribers(author.getUsername())) { for (User u : subscriptionService.getSubscribers(author.getUsername())) {
@@ -124,6 +124,7 @@ public class PostService {
null, null,
null, null,
author, author,
null,
null); null);
} }
} }
@@ -151,9 +152,9 @@ public class PostService {
if (viewer != null && !viewer.equals(post.getAuthor().getUsername())) { if (viewer != null && !viewer.equals(post.getAuthor().getUsername())) {
User viewerUser = userRepository.findByUsername(viewer).orElse(null); User viewerUser = userRepository.findByUsername(viewer).orElse(null);
if (viewerUser != null) { if (viewerUser != null) {
notificationService.createNotification(post.getAuthor(), NotificationType.POST_VIEWED, post, null, null, viewerUser, null); notificationService.createNotification(post.getAuthor(), NotificationType.POST_VIEWED, post, null, null, viewerUser, null, null);
} else { } else {
notificationService.createNotification(post.getAuthor(), NotificationType.POST_VIEWED, post, null, null); notificationService.createNotification(post.getAuthor(), NotificationType.POST_VIEWED, post, null, null, null, null, null);
} }
} }
return post; return post;
@@ -321,7 +322,7 @@ public class PostService {
} }
post.setStatus(PostStatus.PUBLISHED); post.setStatus(PostStatus.PUBLISHED);
post = postRepository.save(post); post = postRepository.save(post);
notificationService.createNotification(post.getAuthor(), NotificationType.POST_REVIEWED, post, null, true); notificationService.createNotification(post.getAuthor(), NotificationType.POST_REVIEWED, post, null, true, null, null, null);
return post; return post;
} }
@@ -341,7 +342,7 @@ public class PostService {
} }
post.setStatus(PostStatus.REJECTED); post.setStatus(PostStatus.REJECTED);
post = postRepository.save(post); post = postRepository.save(post);
notificationService.createNotification(post.getAuthor(), NotificationType.POST_REVIEWED, post, null, false); notificationService.createNotification(post.getAuthor(), NotificationType.POST_REVIEWED, post, null, false, null, null, null);
return post; return post;
} }

View File

@@ -40,7 +40,7 @@ public class ReactionService {
reaction.setType(type); reaction.setType(type);
reaction = reactionRepository.save(reaction); reaction = reactionRepository.save(reaction);
if (!user.getId().equals(post.getAuthor().getId())) { if (!user.getId().equals(post.getAuthor().getId())) {
notificationService.createNotification(post.getAuthor(), NotificationType.REACTION, post, null, null, user, type); notificationService.createNotification(post.getAuthor(), NotificationType.REACTION, post, null, null, user, type, null);
} }
return reaction; return reaction;
} }
@@ -63,7 +63,7 @@ public class ReactionService {
reaction.setType(type); reaction.setType(type);
reaction = reactionRepository.save(reaction); reaction = reactionRepository.save(reaction);
if (!user.getId().equals(comment.getAuthor().getId())) { if (!user.getId().equals(comment.getAuthor().getId())) {
notificationService.createNotification(comment.getAuthor(), NotificationType.REACTION, comment.getPost(), comment, null, user, type); notificationService.createNotification(comment.getAuthor(), NotificationType.REACTION, comment.getPost(), comment, null, user, type, null);
} }
return reaction; return reaction;
} }

View File

@@ -0,0 +1,25 @@
package com.openisle.service;
import com.openisle.model.RegisterMode;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* Holds current registration mode. Configurable at runtime.
*/
@Service
public class RegisterModeService {
private RegisterMode registerMode;
public RegisterModeService(@Value("${app.register.mode:WHITELIST}") RegisterMode registerMode) {
this.registerMode = registerMode;
}
public RegisterMode getRegisterMode() {
return registerMode;
}
public void setRegisterMode(RegisterMode mode) {
this.registerMode = mode;
}
}

View File

@@ -28,7 +28,7 @@ public class SubscriptionService {
ps.setPost(post); ps.setPost(post);
if (!user.getId().equals(post.getAuthor().getId())) { if (!user.getId().equals(post.getAuthor().getId())) {
notificationService.createNotification(post.getAuthor(), notificationService.createNotification(post.getAuthor(),
NotificationType.POST_SUBSCRIBED, post, null, null, user, null); NotificationType.POST_SUBSCRIBED, post, null, null, user, null, null);
} }
return postSubRepo.save(ps); return postSubRepo.save(ps);
}); });
@@ -41,7 +41,7 @@ public class SubscriptionService {
postSubRepo.delete(ps); postSubRepo.delete(ps);
if (!user.getId().equals(post.getAuthor().getId())) { if (!user.getId().equals(post.getAuthor().getId())) {
notificationService.createNotification(post.getAuthor(), notificationService.createNotification(post.getAuthor(),
NotificationType.POST_UNSUBSCRIBED, post, null, null, user, null); NotificationType.POST_UNSUBSCRIBED, post, null, null, user, null, null);
} }
}); });
} }
@@ -72,7 +72,7 @@ public class SubscriptionService {
us.setSubscriber(subscriber); us.setSubscriber(subscriber);
us.setTarget(target); us.setTarget(target);
notificationService.createNotification(target, notificationService.createNotification(target,
NotificationType.USER_FOLLOWED, null, null, null, subscriber, null); NotificationType.USER_FOLLOWED, null, null, null, subscriber, null, null);
return userSubRepo.save(us); return userSubRepo.save(us);
}); });
} }
@@ -83,7 +83,7 @@ public class SubscriptionService {
userSubRepo.findBySubscriberAndTarget(subscriber, target).ifPresent(us -> { userSubRepo.findBySubscriberAndTarget(subscriber, target).ifPresent(us -> {
userSubRepo.delete(us); userSubRepo.delete(us);
notificationService.createNotification(target, notificationService.createNotification(target,
NotificationType.USER_UNFOLLOWED, null, null, null, subscriber, null); NotificationType.USER_UNFOLLOWED, null, null, null, subscriber, null, null);
}); });
} }

View File

@@ -22,7 +22,7 @@ public class UserService {
private final UsernameValidator usernameValidator; private final UsernameValidator usernameValidator;
private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder(); private final PasswordEncoder passwordEncoder = new BCryptPasswordEncoder();
public User register(String username, String email, String password) { 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);
// ── 先按用户名查 ────────────────────────────────────────── // ── 先按用户名查 ──────────────────────────────────────────
@@ -36,6 +36,8 @@ 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.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(u); return userRepository.save(u);
} }
@@ -50,6 +52,8 @@ 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.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(u); return userRepository.save(u);
} }
@@ -62,6 +66,8 @@ public class UserService {
user.setVerified(false); user.setVerified(false);
user.setVerificationCode(genCode()); user.setVerificationCode(genCode());
user.setAvatar("https://github.com/identicons/" + username + ".png"); user.setAvatar("https://github.com/identicons/" + username + ".png");
user.setRegisterReason(reason);
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
return userRepository.save(user); return userRepository.save(user);
} }
@@ -84,6 +90,7 @@ public class UserService {
public Optional<User> authenticate(String username, String password) { public Optional<User> authenticate(String username, String password) {
return userRepository.findByUsername(username) return userRepository.findByUsername(username)
.filter(User::isVerified) .filter(User::isVerified)
.filter(User::isApproved)
.filter(user -> passwordEncoder.matches(password, user.getPassword())); .filter(user -> passwordEncoder.matches(password, user.getPassword()));
} }

View File

@@ -13,6 +13,9 @@ app.password.strength=${PASSWORD_STRENGTH:LOW}
# Post publish mode: DIRECT or REVIEW # Post publish mode: DIRECT or REVIEW
app.post.publish-mode=${POST_PUBLISH_MODE:DIRECT} app.post.publish-mode=${POST_PUBLISH_MODE:DIRECT}
# User register mode: DIRECT or WHITELIST
app.register.mode=${REGISTER_MODE:WHITELIST}
# Image upload configuration # Image upload configuration
app.upload.check-type=${UPLOAD_CHECK_TYPE:true} app.upload.check-type=${UPLOAD_CHECK_TYPE:true}
app.upload.max-size=${UPLOAD_MAX_SIZE:5242880} app.upload.max-size=${UPLOAD_MAX_SIZE:5242880}

View File

@@ -2,6 +2,7 @@ package com.openisle.controller;
import com.openisle.model.User; import com.openisle.model.User;
import com.openisle.service.*; import com.openisle.service.*;
import com.openisle.model.RegisterMode;
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;
@@ -36,6 +37,8 @@ class AuthControllerTest {
private CaptchaService captchaService; private CaptchaService captchaService;
@MockBean @MockBean
private GoogleAuthService googleAuthService; private GoogleAuthService googleAuthService;
@MockBean
private RegisterModeService registerModeService;
@Test @Test
void registerSendsEmail() throws Exception { void registerSendsEmail() throws Exception {
@@ -43,11 +46,12 @@ class AuthControllerTest {
user.setEmail("a@b.com"); user.setEmail("a@b.com");
user.setUsername("u"); user.setUsername("u");
user.setVerificationCode("123456"); user.setVerificationCode("123456");
Mockito.when(userService.register(eq("u"), eq("a@b.com"), eq("p"))).thenReturn(user); Mockito.when(registerModeService.getRegisterMode()).thenReturn(RegisterMode.DIRECT);
Mockito.when(userService.register(eq("u"), eq("a@b.com"), eq("p"), any(), eq(RegisterMode.DIRECT))).thenReturn(user);
mockMvc.perform(post("/api/auth/register") mockMvc.perform(post("/api/auth/register")
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"u\",\"email\":\"a@b.com\",\"password\":\"p\"}")) .content("{\"username\":\"u\",\"email\":\"a@b.com\",\"password\":\"p\",\"reason\":\"test reason more than twenty\"}"))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.message").exists()); .andExpect(jsonPath("$.message").exists());

View File

@@ -17,7 +17,8 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "app.register.mode=DIRECT")
class ComplexFlowIntegrationTest { class ComplexFlowIntegrationTest {
@Autowired @Autowired
@@ -33,7 +34,7 @@ class ComplexFlowIntegrationTest {
HttpHeaders h = new HttpHeaders(); HttpHeaders h = new HttpHeaders();
h.setContentType(MediaType.APPLICATION_JSON); h.setContentType(MediaType.APPLICATION_JSON);
rest.postForEntity("/api/auth/register", new HttpEntity<>( rest.postForEntity("/api/auth/register", new HttpEntity<>(
Map.of("username", username, "email", email, "password", "pass123"), h), Map.class); Map.of("username", username, "email", email, "password", "pass123", "reason", "integration test reason more than twenty"), h), Map.class);
User u = users.findByUsername(username).orElseThrow(); User u = users.findByUsername(username).orElseThrow();
if (u.getVerificationCode() != null) { if (u.getVerificationCode() != null) {
rest.postForEntity("/api/auth/verify", new HttpEntity<>( rest.postForEntity("/api/auth/verify", new HttpEntity<>(

View File

@@ -18,7 +18,7 @@ import static org.junit.jupiter.api.Assertions.*;
/** Integration tests for review publish mode. */ /** Integration tests for review publish mode. */
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "app.post.publish-mode=REVIEW") properties = {"app.post.publish-mode=REVIEW","app.register.mode=DIRECT"})
class PublishModeIntegrationTest { class PublishModeIntegrationTest {
@Autowired @Autowired
@@ -34,7 +34,7 @@ class PublishModeIntegrationTest {
HttpHeaders h = new HttpHeaders(); HttpHeaders h = new HttpHeaders();
h.setContentType(MediaType.APPLICATION_JSON); h.setContentType(MediaType.APPLICATION_JSON);
rest.postForEntity("/api/auth/register", new HttpEntity<>( rest.postForEntity("/api/auth/register", new HttpEntity<>(
Map.of("username", username, "email", email, "password", "pass123"), h), Map.class); Map.of("username", username, "email", email, "password", "pass123", "reason", "integration test reason more than twenty"), h), Map.class);
User u = users.findByUsername(username).orElseThrow(); User u = users.findByUsername(username).orElseThrow();
if (u.getVerificationCode() != null) { if (u.getVerificationCode() != null) {
rest.postForEntity("/api/auth/verify", new HttpEntity<>( rest.postForEntity("/api/auth/verify", new HttpEntity<>(

View File

@@ -16,7 +16,8 @@ import java.util.Map;
import static org.junit.jupiter.api.Assertions.*; import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
properties = "app.register.mode=DIRECT")
class SearchIntegrationTest { class SearchIntegrationTest {
@Autowired @Autowired
private TestRestTemplate rest; private TestRestTemplate rest;
@@ -29,7 +30,7 @@ class SearchIntegrationTest {
HttpHeaders h = new HttpHeaders(); HttpHeaders h = new HttpHeaders();
h.setContentType(MediaType.APPLICATION_JSON); h.setContentType(MediaType.APPLICATION_JSON);
rest.postForEntity("/api/auth/register", new HttpEntity<>( rest.postForEntity("/api/auth/register", new HttpEntity<>(
Map.of("username", username, "email", email, "password", "pass123"), h), Map.class); Map.of("username", username, "email", email, "password", "pass123", "reason", "integration test reason more than twenty"), h), Map.class);
User u = users.findByUsername(username).orElseThrow(); User u = users.findByUsername(username).orElseThrow();
if (u.getVerificationCode() != null) { if (u.getVerificationCode() != null) {
rest.postForEntity("/api/auth/verify", new HttpEntity<>( rest.postForEntity("/api/auth/verify", new HttpEntity<>(