mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-02-21 22:41:05 +08:00
Compare commits
1 Commits
feature/fi
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
901b3f344a |
@@ -3,12 +3,6 @@ MYSQL_URL=jdbc:mysql://<数据库地址>:<端口>/<数据库名>?useUnicode=yes&
|
|||||||
MYSQL_USER=<数据库用户名>
|
MYSQL_USER=<数据库用户名>
|
||||||
MYSQL_PASSWORD=<数据库密码>
|
MYSQL_PASSWORD=<数据库密码>
|
||||||
|
|
||||||
# === JWT ===
|
|
||||||
JWT_SECRET=<jwt secret>
|
|
||||||
JWT_REASON_SECRET=<jwt reason secret>
|
|
||||||
JWT_RESET_SECRET=<jwt reset secret>
|
|
||||||
JWT_INVITE_SECRET=<jwt invite secret>
|
|
||||||
JWT_EXPIRATION=2592000000
|
|
||||||
|
|
||||||
# === Resend ===
|
# === Resend ===
|
||||||
RESEND_API_KEY=<你的resend-api-key>
|
RESEND_API_KEY=<你的resend-api-key>
|
||||||
@@ -36,4 +30,4 @@ OPENAI_API_KEY=<你的openai-api-key>
|
|||||||
WEBPUSH_PUBLIC_KEY=<你的webpush-public-key>
|
WEBPUSH_PUBLIC_KEY=<你的webpush-public-key>
|
||||||
WEBPUSH_PRIVATE_KEY=<你的webpush-private-key>
|
WEBPUSH_PRIVATE_KEY=<你的webpush-private-key>
|
||||||
|
|
||||||
# LOG_LEVEL=DEBUG
|
# LOG_LEVEL=DEBUG
|
||||||
@@ -6,7 +6,7 @@ import com.openisle.repository.ActivityRepository;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.boot.CommandLineRunner;
|
import org.springframework.boot.CommandLineRunner;
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
@Component
|
@Component
|
||||||
@@ -29,10 +29,9 @@ public class ActivityInitializer implements CommandLineRunner {
|
|||||||
Activity a = new Activity();
|
Activity a = new Activity();
|
||||||
a.setTitle("🎁邀请码送积分活动");
|
a.setTitle("🎁邀请码送积分活动");
|
||||||
a.setType(ActivityType.INVITE_POINTS);
|
a.setType(ActivityType.INVITE_POINTS);
|
||||||
a.setIcon("https://img.icons8.com/color/96/gift.png");
|
a.setIcon("https://icons.veryicon.com/png/o/commerce-shopping/two-color-icon-library/gift-30.png");
|
||||||
a.setContent("使用邀请码注册或邀请好友即可获得积分奖励,快来参与吧!");
|
a.setContent("活动期间,邀请好友注册可获得积分奖励,快来参与吧!");
|
||||||
a.setStartTime(LocalDateTime.now());
|
a.setEndTime(LocalDateTime.of(2025, 10, 1, 0, 0));
|
||||||
a.setEndTime(LocalDate.of(LocalDate.now().getYear(), 10, 1).atStartOfDay());
|
|
||||||
activityRepository.save(a);
|
activityRepository.save(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ public class AuthController {
|
|||||||
private final RegisterModeService registerModeService;
|
private final RegisterModeService registerModeService;
|
||||||
private final NotificationService notificationService;
|
private final NotificationService notificationService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
private final InviteService inviteService;
|
|
||||||
|
|
||||||
|
|
||||||
@Value("${app.captcha.enabled:false}")
|
@Value("${app.captcha.enabled:false}")
|
||||||
@@ -46,26 +45,6 @@ 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"));
|
||||||
}
|
}
|
||||||
if (req.getInviteToken() != null && !req.getInviteToken().isEmpty()) {
|
|
||||||
if (!inviteService.validate(req.getInviteToken())) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "邀请码使用次数过多"));
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
User user = userService.registerWithInvite(
|
|
||||||
req.getUsername(), req.getEmail(), req.getPassword());
|
|
||||||
inviteService.consume(req.getInviteToken());
|
|
||||||
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"token", jwtService.generateToken(user.getUsername()),
|
|
||||||
"reason_code", "INVITE_APPROVED"
|
|
||||||
));
|
|
||||||
} catch (FieldException e) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
|
||||||
"field", e.getField(),
|
|
||||||
"error", e.getMessage()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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());
|
emailService.sendEmail(user.getEmail(), "在网站填写验证码以验证", "您的验证码是 " + user.getVerificationCode());
|
||||||
@@ -79,26 +58,10 @@ public class AuthController {
|
|||||||
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
public ResponseEntity<?> verify(@RequestBody VerifyRequest req) {
|
||||||
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
|
boolean ok = userService.verifyCode(req.getUsername(), req.getCode());
|
||||||
if (ok) {
|
if (ok) {
|
||||||
Optional<User> userOpt = userService.findByUsername(req.getUsername());
|
return ResponseEntity.ok(Map.of(
|
||||||
if (userOpt.isEmpty()) {
|
"message", "Verified",
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid credentials"));
|
"token", jwtService.generateReasonToken(req.getUsername())
|
||||||
}
|
));
|
||||||
|
|
||||||
User user = userOpt.get();
|
|
||||||
|
|
||||||
if (user.isApproved()) {
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"message", "Verified and isApproved",
|
|
||||||
"reason_code", "VERIFIED_AND_APPROVED",
|
|
||||||
"token", jwtService.generateToken(req.getUsername())
|
|
||||||
));
|
|
||||||
} else {
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"message", "Verified",
|
|
||||||
"reason_code", "VERIFIED",
|
|
||||||
"token", jwtService.generateReasonToken(req.getUsername())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
return ResponseEntity.badRequest().body(Map.of("error", "Invalid verification code"));
|
||||||
}
|
}
|
||||||
@@ -143,42 +106,27 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/google")
|
@PostMapping("/google")
|
||||||
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
public ResponseEntity<?> loginWithGoogle(@RequestBody GoogleLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
Optional<User> user = googleAuthService.authenticate(req.getIdToken(), registerModeService.getRegisterMode());
|
||||||
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
|
if (user.isPresent()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
|
||||||
}
|
|
||||||
Optional<AuthResult> resultOpt = googleAuthService.authenticate(
|
|
||||||
req.getIdToken(),
|
|
||||||
registerModeService.getRegisterMode(),
|
|
||||||
viaInvite);
|
|
||||||
if (resultOpt.isPresent()) {
|
|
||||||
AuthResult result = resultOpt.get();
|
|
||||||
if (viaInvite && result.isNewUser()) {
|
|
||||||
inviteService.consume(req.getInviteToken());
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
|
||||||
"reason_code", "INVITE_APPROVED"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!user.get().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "IS_APPROVING",
|
"reason_code", "IS_APPROVING",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "NOT_APPROVED",
|
"reason_code", "NOT_APPROVED",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Invalid google token",
|
"error", "Invalid google token",
|
||||||
@@ -217,44 +165,28 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/github")
|
@PostMapping("/github")
|
||||||
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
public ResponseEntity<?> loginWithGithub(@RequestBody GithubLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
Optional<User> user = githubAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
|
||||||
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
|
if (user.isPresent()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
|
||||||
}
|
|
||||||
Optional<AuthResult> resultOpt = githubAuthService.authenticate(
|
|
||||||
req.getCode(),
|
|
||||||
registerModeService.getRegisterMode(),
|
|
||||||
req.getRedirectUri(),
|
|
||||||
viaInvite);
|
|
||||||
if (resultOpt.isPresent()) {
|
|
||||||
AuthResult result = resultOpt.get();
|
|
||||||
if (viaInvite && result.isNewUser()) {
|
|
||||||
inviteService.consume(req.getInviteToken());
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
|
||||||
"reason_code", "INVITE_APPROVED"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!user.get().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||||
// 已填写注册理由
|
// 已填写注册理由
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "IS_APPROVING",
|
"reason_code", "IS_APPROVING",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "NOT_APPROVED",
|
"reason_code", "NOT_APPROVED",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Invalid github code",
|
"error", "Invalid github code",
|
||||||
@@ -264,43 +196,27 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/discord")
|
@PostMapping("/discord")
|
||||||
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
public ResponseEntity<?> loginWithDiscord(@RequestBody DiscordLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
Optional<User> user = discordAuthService.authenticate(req.getCode(), registerModeService.getRegisterMode(), req.getRedirectUri());
|
||||||
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
|
if (user.isPresent()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
|
||||||
}
|
|
||||||
Optional<AuthResult> resultOpt = discordAuthService.authenticate(
|
|
||||||
req.getCode(),
|
|
||||||
registerModeService.getRegisterMode(),
|
|
||||||
req.getRedirectUri(),
|
|
||||||
viaInvite);
|
|
||||||
if (resultOpt.isPresent()) {
|
|
||||||
AuthResult result = resultOpt.get();
|
|
||||||
if (viaInvite && result.isNewUser()) {
|
|
||||||
inviteService.consume(req.getInviteToken());
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
|
||||||
"reason_code", "INVITE_APPROVED"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!user.get().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "IS_APPROVING",
|
"reason_code", "IS_APPROVING",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "NOT_APPROVED",
|
"reason_code", "NOT_APPROVED",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Invalid discord code",
|
"error", "Invalid discord code",
|
||||||
@@ -310,44 +226,31 @@ public class AuthController {
|
|||||||
|
|
||||||
@PostMapping("/twitter")
|
@PostMapping("/twitter")
|
||||||
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
public ResponseEntity<?> loginWithTwitter(@RequestBody TwitterLoginRequest req) {
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
Optional<User> user = twitterAuthService.authenticate(
|
||||||
if (viaInvite && !inviteService.validate(req.getInviteToken())) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
|
||||||
}
|
|
||||||
Optional<AuthResult> resultOpt = twitterAuthService.authenticate(
|
|
||||||
req.getCode(),
|
req.getCode(),
|
||||||
req.getCodeVerifier(),
|
req.getCodeVerifier(),
|
||||||
registerModeService.getRegisterMode(),
|
registerModeService.getRegisterMode(),
|
||||||
req.getRedirectUri(),
|
req.getRedirectUri());
|
||||||
viaInvite);
|
if (user.isPresent()) {
|
||||||
if (resultOpt.isPresent()) {
|
|
||||||
AuthResult result = resultOpt.get();
|
|
||||||
if (viaInvite && result.isNewUser()) {
|
|
||||||
inviteService.consume(req.getInviteToken());
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
|
||||||
"reason_code", "INVITE_APPROVED"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||||
}
|
}
|
||||||
if (!result.getUser().isApproved()) {
|
if (!user.get().isApproved()) {
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
if (user.get().getRegisterReason() != null && !user.get().getRegisterReason().isEmpty()) {
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "IS_APPROVING",
|
"reason_code", "IS_APPROVING",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Account awaiting approval",
|
"error", "Account awaiting approval",
|
||||||
"reason_code", "NOT_APPROVED",
|
"reason_code", "NOT_APPROVED",
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
"token", jwtService.generateReasonToken(user.get().getUsername())
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(user.get().getUsername())));
|
||||||
}
|
}
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
return ResponseEntity.badRequest().body(Map.of(
|
||||||
"error", "Invalid twitter code",
|
"error", "Invalid twitter code",
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package com.openisle.controller;
|
|
||||||
|
|
||||||
import com.openisle.service.InviteService;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.security.core.Authentication;
|
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/invite")
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InviteController {
|
|
||||||
private final InviteService inviteService;
|
|
||||||
|
|
||||||
@PostMapping("/generate")
|
|
||||||
public Map<String, String> generate(Authentication auth) {
|
|
||||||
String token = inviteService.generate(auth.getName());
|
|
||||||
return Map.of("token", token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -7,5 +7,4 @@ import lombok.Data;
|
|||||||
public class DiscordLoginRequest {
|
public class DiscordLoginRequest {
|
||||||
private String code;
|
private String code;
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
private String inviteToken;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,4 @@ import lombok.Data;
|
|||||||
public class GithubLoginRequest {
|
public class GithubLoginRequest {
|
||||||
private String code;
|
private String code;
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
private String inviteToken;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,5 +6,4 @@ import lombok.Data;
|
|||||||
@Data
|
@Data
|
||||||
public class GoogleLoginRequest {
|
public class GoogleLoginRequest {
|
||||||
private String idToken;
|
private String idToken;
|
||||||
private String inviteToken;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,5 +9,4 @@ public class RegisterRequest {
|
|||||||
private String email;
|
private String email;
|
||||||
private String password;
|
private String password;
|
||||||
private String captcha;
|
private String captcha;
|
||||||
private String inviteToken;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,5 +8,4 @@ public class TwitterLoginRequest {
|
|||||||
private String code;
|
private String code;
|
||||||
private String redirectUri;
|
private String redirectUri;
|
||||||
private String codeVerifier;
|
private String codeVerifier;
|
||||||
private String inviteToken;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
package com.openisle.model;
|
|
||||||
|
|
||||||
import jakarta.persistence.*;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Invite token entity tracking usage counts.
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Entity
|
|
||||||
public class InviteToken {
|
|
||||||
@Id
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
@ManyToOne
|
|
||||||
private User inviter;
|
|
||||||
|
|
||||||
private LocalDate createdDate;
|
|
||||||
|
|
||||||
private int usageCount;
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.openisle.repository;
|
|
||||||
|
|
||||||
import com.openisle.model.InviteToken;
|
|
||||||
import com.openisle.model.User;
|
|
||||||
import org.springframework.data.jpa.repository.JpaRepository;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
public interface InviteTokenRepository extends JpaRepository<InviteToken, String> {
|
|
||||||
Optional<InviteToken> findByInviterAndCreatedDate(User inviter, LocalDate createdDate);
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
package com.openisle.service;
|
|
||||||
|
|
||||||
import com.openisle.model.User;
|
|
||||||
import lombok.Value;
|
|
||||||
|
|
||||||
/** Result for OAuth authentication indicating whether a new user was created. */
|
|
||||||
@Value
|
|
||||||
public class AuthResult {
|
|
||||||
User user;
|
|
||||||
boolean newUser;
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -26,7 +26,7 @@ public class DiscordAuthService {
|
|||||||
@Value("${discord.client-secret:}")
|
@Value("${discord.client-secret:}")
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
|
|
||||||
public Optional<AuthResult> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri, boolean viaInvite) {
|
public Optional<User> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri) {
|
||||||
try {
|
try {
|
||||||
String tokenUrl = "https://discord.com/api/oauth2/token";
|
String tokenUrl = "https://discord.com/api/oauth2/token";
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
@@ -67,13 +67,13 @@ public class DiscordAuthService {
|
|||||||
if (email == null) {
|
if (email == null) {
|
||||||
email = (username != null ? username : id) + "@users.noreply.discord.com";
|
email = (username != null ? username : id) + "@users.noreply.discord.com";
|
||||||
}
|
}
|
||||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
return Optional.of(processUser(email, username, avatar, mode));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthResult processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
private User processUser(String email, String username, String avatar, 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();
|
||||||
@@ -82,7 +82,7 @@ public class DiscordAuthService {
|
|||||||
user.setVerificationCode(null);
|
user.setVerificationCode(null);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
return new AuthResult(user, false);
|
return user;
|
||||||
}
|
}
|
||||||
String baseUsername = username != null ? username : email.split("@")[0];
|
String baseUsername = username != null ? username : email.split("@")[0];
|
||||||
String finalUsername = baseUsername;
|
String finalUsername = baseUsername;
|
||||||
@@ -96,12 +96,12 @@ public class DiscordAuthService {
|
|||||||
user.setPassword("");
|
user.setPassword("");
|
||||||
user.setRole(Role.USER);
|
user.setRole(Role.USER);
|
||||||
user.setVerified(true);
|
user.setVerified(true);
|
||||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
user.setAvatar(avatar);
|
user.setAvatar(avatar);
|
||||||
} else {
|
} else {
|
||||||
user.setAvatar("https://cdn.discordapp.com/embed/avatars/0.png");
|
user.setAvatar("https://cdn.discordapp.com/embed/avatars/0.png");
|
||||||
}
|
}
|
||||||
return new AuthResult(userRepository.save(user), true);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ public class GithubAuthService {
|
|||||||
@Value("${github.client-secret:}")
|
@Value("${github.client-secret:}")
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
|
|
||||||
public Optional<AuthResult> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri, boolean viaInvite) {
|
public Optional<User> authenticate(String code, com.openisle.model.RegisterMode mode, String redirectUri) {
|
||||||
try {
|
try {
|
||||||
String tokenUrl = "https://github.com/login/oauth/access_token";
|
String tokenUrl = "https://github.com/login/oauth/access_token";
|
||||||
HttpHeaders headers = new HttpHeaders();
|
HttpHeaders headers = new HttpHeaders();
|
||||||
@@ -86,13 +86,13 @@ public class GithubAuthService {
|
|||||||
if (email == null) {
|
if (email == null) {
|
||||||
email = username + "@users.noreply.github.com";
|
email = username + "@users.noreply.github.com";
|
||||||
}
|
}
|
||||||
return Optional.of(processUser(email, username, avatarUrl, mode, viaInvite));
|
return Optional.of(processUser(email, username, avatarUrl, mode));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthResult processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
private User processUser(String email, String username, String avatar, 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();
|
||||||
@@ -101,7 +101,7 @@ public class GithubAuthService {
|
|||||||
user.setVerificationCode(null);
|
user.setVerificationCode(null);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
return new AuthResult(user, false);
|
return user;
|
||||||
}
|
}
|
||||||
String baseUsername = username != null ? username : email.split("@")[0];
|
String baseUsername = username != null ? username : email.split("@")[0];
|
||||||
String finalUsername = baseUsername;
|
String finalUsername = baseUsername;
|
||||||
@@ -115,12 +115,12 @@ public class GithubAuthService {
|
|||||||
user.setPassword("");
|
user.setPassword("");
|
||||||
user.setRole(Role.USER);
|
user.setRole(Role.USER);
|
||||||
user.setVerified(true);
|
user.setVerified(true);
|
||||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
user.setAvatar(avatar);
|
user.setAvatar(avatar);
|
||||||
} else {
|
} else {
|
||||||
user.setAvatar(avatarGenerator.generate(finalUsername));
|
user.setAvatar(avatarGenerator.generate(finalUsername));
|
||||||
}
|
}
|
||||||
return new AuthResult(userRepository.save(user), true);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ public class GoogleAuthService {
|
|||||||
@Value("${google.client-id:}")
|
@Value("${google.client-id:}")
|
||||||
private String clientId;
|
private String clientId;
|
||||||
|
|
||||||
public Optional<AuthResult> authenticate(String idTokenString, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
public Optional<User> authenticate(String idTokenString, 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();
|
||||||
@@ -38,13 +38,13 @@ public class GoogleAuthService {
|
|||||||
String email = payload.getEmail();
|
String email = payload.getEmail();
|
||||||
String name = (String) payload.get("name");
|
String name = (String) payload.get("name");
|
||||||
String picture = (String) payload.get("picture");
|
String picture = (String) payload.get("picture");
|
||||||
return Optional.of(processUser(email, name, picture, mode, viaInvite));
|
return Optional.of(processUser(email, name, picture, mode));
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthResult processUser(String email, String name, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
private User processUser(String email, String name, String avatar, 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();
|
||||||
@@ -53,7 +53,8 @@ public class GoogleAuthService {
|
|||||||
user.setVerificationCode(null);
|
user.setVerificationCode(null);
|
||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
return new AuthResult(user, false);
|
|
||||||
|
return user;
|
||||||
}
|
}
|
||||||
User user = new User();
|
User user = new User();
|
||||||
String baseUsername = email.split("@")[0];
|
String baseUsername = email.split("@")[0];
|
||||||
@@ -67,12 +68,12 @@ public class GoogleAuthService {
|
|||||||
user.setPassword("");
|
user.setPassword("");
|
||||||
user.setRole(Role.USER);
|
user.setRole(Role.USER);
|
||||||
user.setVerified(true);
|
user.setVerified(true);
|
||||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
user.setAvatar(avatar);
|
user.setAvatar(avatar);
|
||||||
} else {
|
} else {
|
||||||
user.setAvatar(avatarGenerator.generate(username));
|
user.setAvatar(avatarGenerator.generate(username));
|
||||||
}
|
}
|
||||||
return new AuthResult(userRepository.save(user), true);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +0,0 @@
|
|||||||
package com.openisle.service;
|
|
||||||
|
|
||||||
import com.openisle.model.InviteToken;
|
|
||||||
import com.openisle.model.User;
|
|
||||||
import com.openisle.repository.InviteTokenRepository;
|
|
||||||
import com.openisle.repository.UserRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.LocalDate;
|
|
||||||
import java.util.Optional;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class InviteService {
|
|
||||||
private final InviteTokenRepository inviteTokenRepository;
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final JwtService jwtService;
|
|
||||||
private final PointService pointService;
|
|
||||||
|
|
||||||
public String generate(String username) {
|
|
||||||
User inviter = userRepository.findByUsername(username).orElseThrow();
|
|
||||||
LocalDate today = LocalDate.now();
|
|
||||||
Optional<InviteToken> existing = inviteTokenRepository.findByInviterAndCreatedDate(inviter, today);
|
|
||||||
if (existing.isPresent()) {
|
|
||||||
return existing.get().getToken();
|
|
||||||
}
|
|
||||||
String token = jwtService.generateInviteToken(username);
|
|
||||||
InviteToken inviteToken = new InviteToken();
|
|
||||||
inviteToken.setToken(token);
|
|
||||||
inviteToken.setInviter(inviter);
|
|
||||||
inviteToken.setCreatedDate(today);
|
|
||||||
inviteToken.setUsageCount(0);
|
|
||||||
inviteTokenRepository.save(inviteToken);
|
|
||||||
return token;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean validate(String token) {
|
|
||||||
try {
|
|
||||||
jwtService.validateAndGetSubjectForInvite(token);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
|
|
||||||
return invite != null && invite.getUsageCount() < 3;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void consume(String token) {
|
|
||||||
InviteToken invite = inviteTokenRepository.findById(token).orElseThrow();
|
|
||||||
invite.setUsageCount(invite.getUsageCount() + 1);
|
|
||||||
inviteTokenRepository.save(invite);
|
|
||||||
pointService.awardForInvite(invite.getInviter().getUsername());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,9 +24,6 @@ public class JwtService {
|
|||||||
@Value("${app.jwt.reset-secret}")
|
@Value("${app.jwt.reset-secret}")
|
||||||
private String resetSecret;
|
private String resetSecret;
|
||||||
|
|
||||||
@Value("${app.jwt.invite-secret}")
|
|
||||||
private String inviteSecret;
|
|
||||||
|
|
||||||
@Value("${app.jwt.expiration}")
|
@Value("${app.jwt.expiration}")
|
||||||
private long expiration;
|
private long expiration;
|
||||||
|
|
||||||
@@ -73,17 +70,6 @@ public class JwtService {
|
|||||||
.compact();
|
.compact();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String generateInviteToken(String subject) {
|
|
||||||
Date now = new Date();
|
|
||||||
Date expiryDate = new Date(now.getTime() + expiration);
|
|
||||||
return Jwts.builder()
|
|
||||||
.setSubject(subject)
|
|
||||||
.setIssuedAt(now)
|
|
||||||
.setExpiration(expiryDate)
|
|
||||||
.signWith(getSigningKeyForSecret(inviteSecret))
|
|
||||||
.compact();
|
|
||||||
}
|
|
||||||
|
|
||||||
public String validateAndGetSubject(String token) {
|
public String validateAndGetSubject(String token) {
|
||||||
Claims claims = Jwts.parserBuilder()
|
Claims claims = Jwts.parserBuilder()
|
||||||
.setSigningKey(getSigningKeyForSecret(secret))
|
.setSigningKey(getSigningKeyForSecret(secret))
|
||||||
@@ -110,13 +96,4 @@ public class JwtService {
|
|||||||
.getBody();
|
.getBody();
|
||||||
return claims.getSubject();
|
return claims.getSubject();
|
||||||
}
|
}
|
||||||
|
|
||||||
public String validateAndGetSubjectForInvite(String token) {
|
|
||||||
Claims claims = Jwts.parserBuilder()
|
|
||||||
.setSigningKey(getSigningKeyForSecret(inviteSecret))
|
|
||||||
.build()
|
|
||||||
.parseClaimsJws(token)
|
|
||||||
.getBody();
|
|
||||||
return claims.getSubject();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,11 +26,6 @@ public class PointService {
|
|||||||
return addPoint(user, 30);
|
return addPoint(user, 30);
|
||||||
}
|
}
|
||||||
|
|
||||||
public int awardForInvite(String userName) {
|
|
||||||
User user = userRepository.findByUsername(userName).orElseThrow();
|
|
||||||
return addPoint(user, 500);
|
|
||||||
}
|
|
||||||
|
|
||||||
private PointLog getTodayLog(User user) {
|
private PointLog getTodayLog(User user) {
|
||||||
LocalDate today = LocalDate.now();
|
LocalDate today = LocalDate.now();
|
||||||
return pointLogRepository.findByUserAndLogDate(user, today)
|
return pointLogRepository.findByUserAndLogDate(user, today)
|
||||||
|
|||||||
@@ -33,12 +33,11 @@ public class TwitterAuthService {
|
|||||||
@Value("${twitter.client-secret:}")
|
@Value("${twitter.client-secret:}")
|
||||||
private String clientSecret;
|
private String clientSecret;
|
||||||
|
|
||||||
public Optional<AuthResult> authenticate(
|
public Optional<User> authenticate(
|
||||||
String code,
|
String code,
|
||||||
String codeVerifier,
|
String codeVerifier,
|
||||||
RegisterMode mode,
|
RegisterMode mode,
|
||||||
String redirectUri,
|
String redirectUri) {
|
||||||
boolean viaInvite) {
|
|
||||||
|
|
||||||
logger.debug("Starting authentication with code {} and verifier {}", code, codeVerifier);
|
logger.debug("Starting authentication with code {} and verifier {}", code, codeVerifier);
|
||||||
|
|
||||||
@@ -107,10 +106,10 @@ public class TwitterAuthService {
|
|||||||
// Twitter v2 默认拿不到 email;如果你申请到 email.scope,可改用 /2/users/:id?user.fields=email
|
// Twitter v2 默认拿不到 email;如果你申请到 email.scope,可改用 /2/users/:id?user.fields=email
|
||||||
String email = username + "@twitter.com";
|
String email = username + "@twitter.com";
|
||||||
logger.debug("Processing user {} with email {}", username, email);
|
logger.debug("Processing user {} with email {}", username, email);
|
||||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
return Optional.of(processUser(email, username, avatar, mode));
|
||||||
}
|
}
|
||||||
|
|
||||||
private AuthResult processUser(String email, String username, String avatar, com.openisle.model.RegisterMode mode, boolean viaInvite) {
|
private User processUser(String email, String username, String avatar, 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();
|
||||||
@@ -120,7 +119,7 @@ public class TwitterAuthService {
|
|||||||
userRepository.save(user);
|
userRepository.save(user);
|
||||||
}
|
}
|
||||||
logger.debug("Existing user {} authenticated", user.getUsername());
|
logger.debug("Existing user {} authenticated", user.getUsername());
|
||||||
return new AuthResult(user, false);
|
return user;
|
||||||
}
|
}
|
||||||
String baseUsername = username != null ? username : email.split("@")[0];
|
String baseUsername = username != null ? username : email.split("@")[0];
|
||||||
String finalUsername = baseUsername;
|
String finalUsername = baseUsername;
|
||||||
@@ -134,13 +133,13 @@ public class TwitterAuthService {
|
|||||||
user.setPassword("");
|
user.setPassword("");
|
||||||
user.setRole(Role.USER);
|
user.setRole(Role.USER);
|
||||||
user.setVerified(true);
|
user.setVerified(true);
|
||||||
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT || viaInvite);
|
user.setApproved(mode == com.openisle.model.RegisterMode.DIRECT);
|
||||||
if (avatar != null) {
|
if (avatar != null) {
|
||||||
user.setAvatar(avatar);
|
user.setAvatar(avatar);
|
||||||
} else {
|
} else {
|
||||||
user.setAvatar("https://twitter.com/" + finalUsername + "/profile_image");
|
user.setAvatar("https://twitter.com/" + finalUsername + "/profile_image");
|
||||||
}
|
}
|
||||||
logger.debug("Creating new user {}", finalUsername);
|
logger.debug("Creating new user {}", finalUsername);
|
||||||
return new AuthResult(userRepository.save(user), true);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,13 +74,6 @@ public class UserService {
|
|||||||
return userRepository.save(user);
|
return userRepository.save(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public User registerWithInvite(String username, String email, String password) {
|
|
||||||
User user = register(username, email, password, "", com.openisle.model.RegisterMode.DIRECT);
|
|
||||||
user.setVerified(true);
|
|
||||||
user.setVerificationCode(genCode());
|
|
||||||
return userRepository.save(user);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String genCode() {
|
private String genCode() {
|
||||||
return String.format("%06d", new Random().nextInt(1000000));
|
return String.format("%06d", new Random().nextInt(1000000));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ spring.jpa.hibernate.ddl-auto=update
|
|||||||
app.jwt.secret=${JWT_SECRET:jwt_sec}
|
app.jwt.secret=${JWT_SECRET:jwt_sec}
|
||||||
app.jwt.reason-secret=${JWT_REASON_SECRET:jwt_reason_sec}
|
app.jwt.reason-secret=${JWT_REASON_SECRET:jwt_reason_sec}
|
||||||
app.jwt.reset-secret=${JWT_RESET_SECRET:jwt_reset_sec}
|
app.jwt.reset-secret=${JWT_RESET_SECRET:jwt_reset_sec}
|
||||||
app.jwt.invite-secret=${JWT_INVITE_SECRET:jwt_invite_sec}
|
|
||||||
# 30 days
|
# 30 days
|
||||||
app.jwt.expiration=${JWT_EXPIRATION:2592000000}
|
app.jwt.expiration=${JWT_EXPIRATION:2592000000}
|
||||||
# Password strength: LOW, MEDIUM or HIGH
|
# Password strength: LOW, MEDIUM or HIGH
|
||||||
|
|||||||
@@ -1,5 +1,11 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
|
<ActivityPopup
|
||||||
|
:visible="showInvitePointsPopup"
|
||||||
|
:icon="invitePointsIcon"
|
||||||
|
text="邀请码送积分活动火热进行中,快来邀请好友吧!"
|
||||||
|
@close="closeInvitePointsPopup"
|
||||||
|
/>
|
||||||
<ActivityPopup
|
<ActivityPopup
|
||||||
:visible="showMilkTeaPopup"
|
:visible="showMilkTeaPopup"
|
||||||
:icon="milkTeaIcon"
|
:icon="milkTeaIcon"
|
||||||
@@ -8,13 +14,6 @@
|
|||||||
/>
|
/>
|
||||||
<NotificationSettingPopup :visible="showNotificationPopup" @close="closeNotificationPopup" />
|
<NotificationSettingPopup :visible="showNotificationPopup" @close="closeNotificationPopup" />
|
||||||
<MedalPopup :visible="showMedalPopup" :medals="newMedals" @close="closeMedalPopup" />
|
<MedalPopup :visible="showMedalPopup" :medals="newMedals" @close="closeMedalPopup" />
|
||||||
|
|
||||||
<ActivityPopup
|
|
||||||
:visible="showInviteCodePopup"
|
|
||||||
:icon="inviteCodeIcon"
|
|
||||||
text="邀请码活动开始了,速来参与大伙们🔥🔥🔥"
|
|
||||||
@close="closeInviteCodePopup"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -27,22 +26,21 @@ import { authState } from '~/utils/auth'
|
|||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
|
const showInvitePointsPopup = ref(false)
|
||||||
|
const invitePointsIcon = ref('')
|
||||||
const showMilkTeaPopup = ref(false)
|
const showMilkTeaPopup = ref(false)
|
||||||
const showInviteCodePopup = ref(false)
|
|
||||||
const milkTeaIcon = ref('')
|
const milkTeaIcon = ref('')
|
||||||
const inviteCodeIcon = ref('')
|
|
||||||
|
|
||||||
const showNotificationPopup = ref(false)
|
const showNotificationPopup = ref(false)
|
||||||
const showMedalPopup = ref(false)
|
const showMedalPopup = ref(false)
|
||||||
const newMedals = ref([])
|
const newMedals = ref([])
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
|
await checkInvitePointsActivity()
|
||||||
|
if (showInvitePointsPopup.value) return
|
||||||
|
|
||||||
await checkMilkTeaActivity()
|
await checkMilkTeaActivity()
|
||||||
if (showMilkTeaPopup.value) return
|
if (showMilkTeaPopup.value) return
|
||||||
|
|
||||||
await checkInviteCodeActivity()
|
|
||||||
if (showInviteCodePopup.value) return
|
|
||||||
|
|
||||||
await checkNotificationSetting()
|
await checkNotificationSetting()
|
||||||
if (showNotificationPopup.value) return
|
if (showNotificationPopup.value) return
|
||||||
|
|
||||||
@@ -66,38 +64,35 @@ const checkMilkTeaActivity = async () => {
|
|||||||
// ignore network errors
|
// ignore network errors
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkInviteCodeActivity = async () => {
|
|
||||||
if (!process.client) return
|
|
||||||
if (localStorage.getItem('inviteCodeActivityPopupShown')) return
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
|
||||||
if (res.ok) {
|
|
||||||
const list = await res.json()
|
|
||||||
const a = list.find((i) => i.type === 'INVITE_POINTS' && !i.ended)
|
|
||||||
if (a) {
|
|
||||||
inviteCodeIcon.value = a.icon
|
|
||||||
showInviteCodePopup.value = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// ignore network errors
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeInviteCodePopup = () => {
|
|
||||||
if (!process.client) return
|
|
||||||
localStorage.setItem('inviteCodeActivityPopupShown', 'true')
|
|
||||||
showInviteCodePopup.value = false
|
|
||||||
}
|
|
||||||
|
|
||||||
const closeMilkTeaPopup = () => {
|
const closeMilkTeaPopup = () => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
localStorage.setItem('milkTeaActivityPopupShown', 'true')
|
localStorage.setItem('milkTeaActivityPopupShown', 'true')
|
||||||
showMilkTeaPopup.value = false
|
showMilkTeaPopup.value = false
|
||||||
checkNotificationSetting()
|
checkNotificationSetting()
|
||||||
}
|
}
|
||||||
|
const checkInvitePointsActivity = async () => {
|
||||||
|
if (!process.client) return
|
||||||
|
if (localStorage.getItem('invitePointsActivityPopupShown')) return
|
||||||
|
try {
|
||||||
|
const res = await fetch(`${API_BASE_URL}/api/activities`)
|
||||||
|
if (res.ok) {
|
||||||
|
const list = await res.json()
|
||||||
|
const a = list.find((i) => i.type === 'INVITE_POINTS' && !i.ended)
|
||||||
|
if (a) {
|
||||||
|
invitePointsIcon.value = a.icon
|
||||||
|
showInvitePointsPopup.value = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// ignore network errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const closeInvitePointsPopup = () => {
|
||||||
|
if (!process.client) return
|
||||||
|
localStorage.setItem('invitePointsActivityPopupShown', 'true')
|
||||||
|
showInvitePointsPopup.value = false
|
||||||
|
checkMilkTeaActivity()
|
||||||
|
}
|
||||||
const checkNotificationSetting = async () => {
|
const checkNotificationSetting = async () => {
|
||||||
if (!process.client) return
|
if (!process.client) return
|
||||||
if (!authState.loggedIn) return
|
if (!authState.loggedIn) return
|
||||||
|
|||||||
@@ -1,190 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div class="invite-code-activity">
|
|
||||||
<div class="invite-code-description">
|
|
||||||
<div class="invite-code-description-title">
|
|
||||||
<i class="fas fa-info-circle"></i>
|
|
||||||
<span class="invite-code-description-title-text">邀请规则说明</span>
|
|
||||||
</div>
|
|
||||||
<div class="invite-code-description-content">
|
|
||||||
<p>⚠️邀请好友注册并登录,每次可以获得500积分🎉🎉🎉</p>
|
|
||||||
<p>邀请链接的有效期为1个月</p>
|
|
||||||
<p>每一个邀请链接的邀请人数上限为3人</p>
|
|
||||||
<p>通过邀请链接注册,无需注册审核</p>
|
|
||||||
<p>每人每天仅能生产1个邀请链接</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="inviteLink" class="invite-code-link-content">
|
|
||||||
<p class="invite-code-link-content-text">
|
|
||||||
邀请链接:{{ inviteLink }}
|
|
||||||
<span @click="copyLink"><i class="fas fa-copy copy-icon"></i></span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div :class="['generate-button', { disabled: !user || loadingInvite }]" @click="generateInvite">
|
|
||||||
生成邀请链接
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { toast } from '~/main'
|
|
||||||
import { fetchCurrentUser, getToken } from '~/utils/auth'
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
|
||||||
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
|
||||||
|
|
||||||
const user = ref(null)
|
|
||||||
const isLoadingUser = ref(true)
|
|
||||||
const inviteCode = ref('')
|
|
||||||
const loadingInvite = ref(false)
|
|
||||||
|
|
||||||
const inviteLink = computed(() =>
|
|
||||||
inviteCode.value ? `${WEBSITE_BASE_URL}/signup?invite_token=${inviteCode.value}` : '',
|
|
||||||
)
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
isLoadingUser.value = true
|
|
||||||
user.value = await fetchCurrentUser()
|
|
||||||
isLoadingUser.value = false
|
|
||||||
// if (user.value) {
|
|
||||||
// await fetchInvite(false)
|
|
||||||
// }
|
|
||||||
})
|
|
||||||
|
|
||||||
const fetchInvite = async (showToast = true) => {
|
|
||||||
loadingInvite.value = true
|
|
||||||
const token = getToken()
|
|
||||||
if (!token) {
|
|
||||||
toast.error('请先登录')
|
|
||||||
loadingInvite.value = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/invite/generate`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { Authorization: `Bearer ${token}` },
|
|
||||||
})
|
|
||||||
if (res.ok) {
|
|
||||||
const data = await res.json()
|
|
||||||
inviteCode.value = data.token
|
|
||||||
if (showToast) toast.success('邀请链接已生成')
|
|
||||||
} else {
|
|
||||||
const data = await res.json().catch(() => ({}))
|
|
||||||
toast.error(data.error || '生成邀请链接失败')
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
toast.error('生成邀请链接失败')
|
|
||||||
} finally {
|
|
||||||
loadingInvite.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const generateInvite = () => fetchInvite(true)
|
|
||||||
|
|
||||||
const copyLink = async () => {
|
|
||||||
if (!inviteLink.value) return
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(inviteLink.value)
|
|
||||||
toast.success('已复制')
|
|
||||||
} catch (e) {
|
|
||||||
toast.error('复制失败')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.invite-code-description-title-text {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: bold;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-code-description-content {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-title {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.status-text {
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-code-activity {
|
|
||||||
margin-top: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-button {
|
|
||||||
margin-top: 20px;
|
|
||||||
background-color: var(--primary-color);
|
|
||||||
color: #fff;
|
|
||||||
padding: 8px 16px;
|
|
||||||
border-radius: 10px;
|
|
||||||
width: fit-content;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-button:hover {
|
|
||||||
background-color: var(--primary-color-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-button.disabled {
|
|
||||||
background-color: var(--primary-color-disabled);
|
|
||||||
cursor: not-allowed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.generate-button.disabled:hover {
|
|
||||||
background-color: var(--primary-color-disabled);
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-code-status-container {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
align-items: center;
|
|
||||||
gap: 30px;
|
|
||||||
margin-top: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-code-status {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 4px;
|
|
||||||
margin-top: 10px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.user-level-text {
|
|
||||||
opacity: 0.8;
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-code-link-content {
|
|
||||||
margin-top: 20px;
|
|
||||||
font-size: 12px;
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
|
|
||||||
.invite-code-link-content-text {
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-icon {
|
|
||||||
cursor: pointer;
|
|
||||||
margin-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
|
||||||
.invite-code-status-container {
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 10px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@@ -12,6 +12,7 @@ export default defineNuxtConfig({
|
|||||||
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// 确保 Vditor 样式在 global.css 覆盖前加载
|
||||||
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
|
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
|
||||||
app: {
|
app: {
|
||||||
pageTransition: { name: 'page', mode: 'out-in' },
|
pageTransition: { name: 'page', mode: 'out-in' },
|
||||||
|
|||||||
@@ -25,7 +25,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<MilkTeaActivityComponent v-if="a.type === 'MILK_TEA'" />
|
<MilkTeaActivityComponent v-if="a.type === 'MILK_TEA'" />
|
||||||
<InviteCodeActivityComponent v-if="a.type === 'INVITE_POINTS'" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -33,7 +32,6 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import TimeManager from '~/utils/time'
|
import TimeManager from '~/utils/time'
|
||||||
import MilkTeaActivityComponent from '~/components/MilkTeaActivityComponent.vue'
|
import MilkTeaActivityComponent from '~/components/MilkTeaActivityComponent.vue'
|
||||||
import InviteCodeActivityComponent from '~/components/InviteCodeActivityComponent.vue'
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
@@ -77,7 +75,6 @@ onMounted(async () => {
|
|||||||
background-color: var(--activity-card-background-color);
|
background-color: var(--activity-card-background-color);
|
||||||
border-radius: 20px;
|
border-radius: 20px;
|
||||||
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
box-shadow: 0 0 10px 0 rgba(0, 0, 0, 0.1);
|
||||||
margin-bottom: 10px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-card-left-avatar-img {
|
.activity-card-left-avatar-img {
|
||||||
@@ -144,10 +141,6 @@ onMounted(async () => {
|
|||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
.activity-card-normal-right {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (max-width: 768px) {
|
@media screen and (max-width: 768px) {
|
||||||
.activity-card-left-avatar-img {
|
.activity-card-left-avatar-img {
|
||||||
width: 80px;
|
width: 80px;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<!-- pages/discord-callback.vue -->
|
|
||||||
<template>
|
<template>
|
||||||
<CallbackPage />
|
<CallbackPage />
|
||||||
</template>
|
</template>
|
||||||
@@ -9,30 +8,9 @@ import { discordExchange } from '~/utils/discord'
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
const code = url.searchParams.get('code') || ''
|
const code = url.searchParams.get('code')
|
||||||
const stateStr = url.searchParams.get('state') || ''
|
const state = url.searchParams.get('state')
|
||||||
|
const result = await discordExchange(code, state, '')
|
||||||
// 从 state 解析 invite_token;兜底支持 query ?invite_token=
|
|
||||||
let inviteToken = ''
|
|
||||||
if (stateStr) {
|
|
||||||
try {
|
|
||||||
const s = new URLSearchParams(stateStr)
|
|
||||||
inviteToken = s.get('invite_token') || s.get('invitetoken') || ''
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
// if (!inviteToken) {
|
|
||||||
// inviteToken =
|
|
||||||
// url.searchParams.get('invite_token') ||
|
|
||||||
// url.searchParams.get('invitetoken') ||
|
|
||||||
// ''
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!code) {
|
|
||||||
navigateTo('/login', { replace: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await discordExchange(code, inviteToken, '')
|
|
||||||
|
|
||||||
if (result.needReason) {
|
if (result.needReason) {
|
||||||
navigateTo(`/signup-reason?token=${result.token}`, { replace: true })
|
navigateTo(`/signup-reason?token=${result.token}`, { replace: true })
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
<!-- pages/github-callback.vue -->
|
|
||||||
<template>
|
<template>
|
||||||
<CallbackPage />
|
<CallbackPage />
|
||||||
</template>
|
</template>
|
||||||
@@ -9,31 +8,9 @@ import { githubExchange } from '~/utils/github'
|
|||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const url = new URL(window.location.href)
|
const url = new URL(window.location.href)
|
||||||
const code = url.searchParams.get('code') || ''
|
const code = url.searchParams.get('code')
|
||||||
const state = url.searchParams.get('state') || ''
|
const state = url.searchParams.get('state')
|
||||||
|
const result = await githubExchange(code, state, '')
|
||||||
// 从 state 中解析 invite_token(githubAuthorize 已把它放进 state)
|
|
||||||
let inviteToken = ''
|
|
||||||
if (state) {
|
|
||||||
try {
|
|
||||||
const s = new URLSearchParams(state)
|
|
||||||
inviteToken = s.get('invite_token') || s.get('invitetoken') || ''
|
|
||||||
} catch {}
|
|
||||||
}
|
|
||||||
// 兜底:也支持直接跟在回调URL的查询参数上
|
|
||||||
// if (!inviteToken) {
|
|
||||||
// inviteToken =
|
|
||||||
// url.searchParams.get('invite_token') ||
|
|
||||||
// url.searchParams.get('invitetoken') ||
|
|
||||||
// ''
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!code) {
|
|
||||||
navigateTo('/login', { replace: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await githubExchange(code, inviteToken, '')
|
|
||||||
|
|
||||||
if (result.needReason) {
|
if (result.needReason) {
|
||||||
navigateTo(`/signup-reason?token=${result.token}`, { replace: true })
|
navigateTo(`/signup-reason?token=${result.token}`, { replace: true })
|
||||||
|
|||||||
@@ -9,21 +9,6 @@ import { googleAuthWithToken } from '~/utils/google'
|
|||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
const hash = new URLSearchParams(window.location.hash.substring(1))
|
const hash = new URLSearchParams(window.location.hash.substring(1))
|
||||||
const idToken = hash.get('id_token')
|
const idToken = hash.get('id_token')
|
||||||
|
|
||||||
// 优先从 state 中解析
|
|
||||||
let inviteToken = ''
|
|
||||||
const stateStr = hash.get('state') || ''
|
|
||||||
if (stateStr) {
|
|
||||||
const state = new URLSearchParams(stateStr)
|
|
||||||
inviteToken = state.get('invite_token') || ''
|
|
||||||
}
|
|
||||||
|
|
||||||
// 兜底:如果之前把 invite_token 放在回调 URL 的查询参数中
|
|
||||||
// if (!inviteToken) {
|
|
||||||
// const query = new URLSearchParams(window.location.search)
|
|
||||||
// inviteToken = query.get('invite_token') || ''
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (idToken) {
|
if (idToken) {
|
||||||
await googleAuthWithToken(
|
await googleAuthWithToken(
|
||||||
idToken,
|
idToken,
|
||||||
@@ -33,7 +18,6 @@ onMounted(async () => {
|
|||||||
(token) => {
|
(token) => {
|
||||||
navigateTo(`/signup-reason?token=${token}`, { replace: true })
|
navigateTo(`/signup-reason?token=${token}`, { replace: true })
|
||||||
},
|
},
|
||||||
{ inviteToken },
|
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
navigateTo('/login', { replace: true })
|
navigateTo('/login', { replace: true })
|
||||||
|
|||||||
@@ -63,7 +63,6 @@ const pointRules = [
|
|||||||
'评论:每天前四条评论可获 10 积分,你的帖子被评论也可获 10 积分',
|
'评论:每天前四条评论可获 10 积分,你的帖子被评论也可获 10 积分',
|
||||||
'帖子被点赞:每次 10 积分',
|
'帖子被点赞:每次 10 积分',
|
||||||
'评论被点赞:每次 10 积分',
|
'评论被点赞:每次 10 积分',
|
||||||
'邀请好友加入可获得 500 积分/次,注意需要使用邀请链接注册',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
const goods = ref([])
|
const goods = ref([])
|
||||||
@@ -129,7 +128,7 @@ const submitRedeem = async () => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.point-mall-page {
|
.point-mall-page {
|
||||||
padding: 0 20px;
|
padding-left: 20px;
|
||||||
max-width: var(--page-max-width);
|
max-width: var(--page-max-width);
|
||||||
background-color: var(--background-color);
|
background-color: var(--background-color);
|
||||||
margin: 0 auto;
|
margin: 0 auto;
|
||||||
|
|||||||
@@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="other-signup-page-content">
|
<div class="other-signup-page-content">
|
||||||
<div class="signup-page-button" @click="signupWithGoogle">
|
<div class="signup-page-button" @click="googleAuthorize">
|
||||||
<img class="signup-page-button-icon" src="~/assets/icons/google.svg" alt="Google Logo" />
|
<img class="signup-page-button-icon" src="~/assets/icons/google.svg" alt="Google Logo" />
|
||||||
<div class="signup-page-button-text">Google 注册</div>
|
<div class="signup-page-button-text">Google 注册</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -96,9 +96,6 @@ import { discordAuthorize } from '~/utils/discord'
|
|||||||
import { githubAuthorize } from '~/utils/github'
|
import { githubAuthorize } from '~/utils/github'
|
||||||
import { googleAuthorize } from '~/utils/google'
|
import { googleAuthorize } from '~/utils/google'
|
||||||
import { twitterAuthorize } from '~/utils/twitter'
|
import { twitterAuthorize } from '~/utils/twitter'
|
||||||
import { loadCurrentUser, setToken } from '~/utils/auth'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
const emailStep = ref(0)
|
const emailStep = ref(0)
|
||||||
@@ -112,11 +109,9 @@ const passwordError = ref('')
|
|||||||
const code = ref('')
|
const code = ref('')
|
||||||
const isWaitingForEmailSent = ref(false)
|
const isWaitingForEmailSent = ref(false)
|
||||||
const isWaitingForEmailVerified = ref(false)
|
const isWaitingForEmailVerified = ref(false)
|
||||||
const inviteToken = ref('')
|
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
username.value = route.query.u || ''
|
username.value = route.query.u || ''
|
||||||
inviteToken.value = route.query.invite_token || ''
|
|
||||||
try {
|
try {
|
||||||
const res = await fetch(`${API_BASE_URL}/api/config`)
|
const res = await fetch(`${API_BASE_URL}/api/config`)
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
@@ -161,7 +156,6 @@ const sendVerification = async () => {
|
|||||||
username: username.value,
|
username: username.value,
|
||||||
email: email.value,
|
email: email.value,
|
||||||
password: password.value,
|
password: password.value,
|
||||||
inviteToken: inviteToken.value,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
isWaitingForEmailSent.value = false
|
isWaitingForEmailSent.value = false
|
||||||
@@ -194,18 +188,11 @@ const verifyCode = async () => {
|
|||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
if (data.reason_code === 'VERIFIED_AND_APPROVED') {
|
if (registerMode.value === 'WHITELIST') {
|
||||||
toast.success('注册成功')
|
navigateTo(`/signup-reason?token=${data.token}`, { replace: true })
|
||||||
setToken(data.token)
|
} else {
|
||||||
loadCurrentUser()
|
toast.success('注册成功,请登录')
|
||||||
navigateTo('/', { replace: true })
|
navigateTo('/login', { replace: true })
|
||||||
} else if (data.reason_code === 'VERIFIED') {
|
|
||||||
if (registerMode.value === 'WHITELIST') {
|
|
||||||
navigateTo(`/signup-reason?token=${data.token}`, { replace: true })
|
|
||||||
} else {
|
|
||||||
toast.success('注册成功,请登录')
|
|
||||||
navigateTo('/login', { replace: true })
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '注册失败')
|
toast.error(data.error || '注册失败')
|
||||||
@@ -216,17 +203,14 @@ const verifyCode = async () => {
|
|||||||
isWaitingForEmailVerified.value = false
|
isWaitingForEmailVerified.value = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const signupWithGoogle = () => {
|
|
||||||
googleAuthorize(inviteToken.value)
|
|
||||||
}
|
|
||||||
const signupWithGithub = () => {
|
const signupWithGithub = () => {
|
||||||
githubAuthorize(inviteToken.value)
|
githubAuthorize()
|
||||||
}
|
}
|
||||||
const signupWithDiscord = () => {
|
const signupWithDiscord = () => {
|
||||||
discordAuthorize(inviteToken.value)
|
discordAuthorize()
|
||||||
}
|
}
|
||||||
const signupWithTwitter = () => {
|
const signupWithTwitter = () => {
|
||||||
twitterAuthorize(inviteToken.value)
|
twitterAuthorize()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { toast } from '../main'
|
|||||||
import { setToken, loadCurrentUser } from './auth'
|
import { setToken, loadCurrentUser } from './auth'
|
||||||
import { registerPush } from './push'
|
import { registerPush } from './push'
|
||||||
|
|
||||||
export function discordAuthorize(inviteToken = '') {
|
export function discordAuthorize(state = '') {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
||||||
const DISCORD_CLIENT_ID = config.public.discordClientId
|
const DISCORD_CLIENT_ID = config.public.discordClientId
|
||||||
@@ -10,60 +10,62 @@ export function discordAuthorize(inviteToken = '') {
|
|||||||
toast.error('Discord 登录不可用')
|
toast.error('Discord 登录不可用')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = `${WEBSITE_BASE_URL}/discord-callback`
|
const redirectUri = `${WEBSITE_BASE_URL}/discord-callback`
|
||||||
// 用 state 明文携带 invite_token(仅用于回传,不再透传给后端)
|
const url = `https://discord.com/api/oauth2/authorize?client_id=${DISCORD_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=code&scope=identify%20email&state=${state}`
|
||||||
const state = new URLSearchParams({ invite_token: inviteToken }).toString()
|
|
||||||
|
|
||||||
const url =
|
|
||||||
`https://discord.com/api/oauth2/authorize` +
|
|
||||||
`?client_id=${encodeURIComponent(DISCORD_CLIENT_ID)}` +
|
|
||||||
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
||||||
`&response_type=code` +
|
|
||||||
`&scope=${encodeURIComponent('identify email')}` +
|
|
||||||
`&state=${encodeURIComponent(state)}`
|
|
||||||
|
|
||||||
window.location.href = url
|
window.location.href = url
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function discordExchange(code, inviteToken = '', reason = '') {
|
export async function discordExchange(code, state, reason) {
|
||||||
try {
|
try {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
const payload = {
|
|
||||||
code,
|
|
||||||
redirectUri: `${window.location.origin}/discord-callback`,
|
|
||||||
reason,
|
|
||||||
}
|
|
||||||
if (inviteToken) payload.inviteToken = inviteToken // 明文传给后端
|
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/discord`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/discord`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify({
|
||||||
|
code,
|
||||||
|
redirectUri: `${window.location.origin}/discord-callback`,
|
||||||
|
reason,
|
||||||
|
state,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (res.ok && data.token) {
|
if (res.ok && data.token) {
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
await loadCurrentUser()
|
await loadCurrentUser()
|
||||||
toast.success('登录成功')
|
toast.success('登录成功')
|
||||||
registerPush?.()
|
registerPush()
|
||||||
return { success: true, needReason: false }
|
return {
|
||||||
|
success: true,
|
||||||
|
needReason: false,
|
||||||
|
}
|
||||||
} else if (data.reason_code === 'NOT_APPROVED') {
|
} else if (data.reason_code === 'NOT_APPROVED') {
|
||||||
toast.info('当前为注册审核模式,请填写注册理由')
|
toast.info('当前为注册审核模式,请填写注册理由')
|
||||||
return { success: false, needReason: true, token: data.token }
|
return {
|
||||||
|
success: false,
|
||||||
|
needReason: true,
|
||||||
|
token: data.token,
|
||||||
|
}
|
||||||
} else if (data.reason_code === 'IS_APPROVING') {
|
} else if (data.reason_code === 'IS_APPROVING') {
|
||||||
toast.info('您的注册理由正在审批中')
|
toast.info('您的注册理由正在审批中')
|
||||||
return { success: true, needReason: false }
|
return {
|
||||||
|
success: true,
|
||||||
|
needReason: false,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '登录失败')
|
toast.error(data.error || '登录失败')
|
||||||
return { success: false, needReason: false, error: data.error || '登录失败' }
|
return {
|
||||||
|
success: false,
|
||||||
|
needReason: false,
|
||||||
|
error: data.error || '登录失败',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
|
||||||
toast.error('登录失败')
|
toast.error('登录失败')
|
||||||
return { success: false, needReason: false, error: '登录失败' }
|
return {
|
||||||
|
success: false,
|
||||||
|
needReason: false,
|
||||||
|
error: '登录失败',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { toast } from '../main'
|
|||||||
import { setToken, loadCurrentUser } from './auth'
|
import { setToken, loadCurrentUser } from './auth'
|
||||||
import { registerPush } from './push'
|
import { registerPush } from './push'
|
||||||
|
|
||||||
export function githubAuthorize(inviteToken = '') {
|
export function githubAuthorize(state = '') {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
||||||
const GITHUB_CLIENT_ID = config.public.githubClientId
|
const GITHUB_CLIENT_ID = config.public.githubClientId
|
||||||
@@ -10,58 +10,62 @@ export function githubAuthorize(inviteToken = '') {
|
|||||||
toast.error('GitHub 登录不可用')
|
toast.error('GitHub 登录不可用')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = `${WEBSITE_BASE_URL}/github-callback`
|
const redirectUri = `${WEBSITE_BASE_URL}/github-callback`
|
||||||
const state = new URLSearchParams({ invite_token: inviteToken }).toString()
|
const url = `https://github.com/login/oauth/authorize?client_id=${GITHUB_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&scope=user:email&state=${state}`
|
||||||
|
|
||||||
const url =
|
|
||||||
`https://github.com/login/oauth/authorize` +
|
|
||||||
`?client_id=${encodeURIComponent(GITHUB_CLIENT_ID)}` +
|
|
||||||
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
||||||
`&scope=${encodeURIComponent('user:email')}` +
|
|
||||||
`&state=${encodeURIComponent(state)}`
|
|
||||||
|
|
||||||
window.location.href = url
|
window.location.href = url
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function githubExchange(code, inviteToken = '', reason = '') {
|
export async function githubExchange(code, state, reason) {
|
||||||
try {
|
try {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
const payload = {
|
|
||||||
code,
|
|
||||||
redirectUri: `${window.location.origin}/github-callback`,
|
|
||||||
reason,
|
|
||||||
}
|
|
||||||
if (inviteToken) payload.inviteToken = inviteToken
|
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/github`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/github`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify({
|
||||||
|
code,
|
||||||
|
redirectUri: `${window.location.origin}/github-callback`,
|
||||||
|
reason,
|
||||||
|
state,
|
||||||
|
}),
|
||||||
})
|
})
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
|
|
||||||
if (res.ok && data.token) {
|
if (res.ok && data.token) {
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
await loadCurrentUser()
|
await loadCurrentUser()
|
||||||
toast.success('登录成功')
|
toast.success('登录成功')
|
||||||
registerPush?.()
|
registerPush()
|
||||||
return { success: true, needReason: false }
|
return {
|
||||||
|
success: true,
|
||||||
|
needReason: false,
|
||||||
|
}
|
||||||
} else if (data.reason_code === 'NOT_APPROVED') {
|
} else if (data.reason_code === 'NOT_APPROVED') {
|
||||||
toast.info('当前为注册审核模式,请填写注册理由')
|
toast.info('当前为注册审核模式,请填写注册理由')
|
||||||
return { success: false, needReason: true, token: data.token }
|
return {
|
||||||
|
success: false,
|
||||||
|
needReason: true,
|
||||||
|
token: data.token,
|
||||||
|
}
|
||||||
} else if (data.reason_code === 'IS_APPROVING') {
|
} else if (data.reason_code === 'IS_APPROVING') {
|
||||||
toast.info('您的注册理由正在审批中')
|
toast.info('您的注册理由正在审批中')
|
||||||
return { success: true, needReason: false }
|
return {
|
||||||
|
success: true,
|
||||||
|
needReason: false,
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
toast.error(data.error || '登录失败')
|
toast.error(data.error || '登录失败')
|
||||||
return { success: false, needReason: false, error: data.error || '登录失败' }
|
return {
|
||||||
|
success: false,
|
||||||
|
needReason: false,
|
||||||
|
error: data.error || '登录失败',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
|
||||||
toast.error('登录失败')
|
toast.error('登录失败')
|
||||||
return { success: false, needReason: false, error: '登录失败' }
|
return {
|
||||||
|
success: false,
|
||||||
|
needReason: false,
|
||||||
|
error: '登录失败',
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,85 +21,44 @@ export async function googleGetIdToken() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export function googleAuthorize(inviteToken = '') {
|
export function googleAuthorize() {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const GOOGLE_CLIENT_ID = config.public.googleClientId
|
const GOOGLE_CLIENT_ID = config.public.googleClientId
|
||||||
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
||||||
|
|
||||||
if (!GOOGLE_CLIENT_ID) {
|
if (!GOOGLE_CLIENT_ID) {
|
||||||
toast.error('Google 登录不可用, 请检查网络设置与VPN')
|
toast.error('Google 登录不可用, 请检查网络设置与VPN')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const redirectUri = `${WEBSITE_BASE_URL}/google-callback`
|
const redirectUri = `${WEBSITE_BASE_URL}/google-callback`
|
||||||
const nonce = Math.random().toString(36).slice(2)
|
const nonce = Math.random().toString(36).substring(2)
|
||||||
|
const url = `https://accounts.google.com/o/oauth2/v2/auth?client_id=${GOOGLE_CLIENT_ID}&redirect_uri=${encodeURIComponent(redirectUri)}&response_type=id_token&scope=openid%20email%20profile&nonce=${nonce}`
|
||||||
// 明文放在 state(推荐;Google 会原样回传)
|
|
||||||
const state = new URLSearchParams({ invite_token: inviteToken }).toString()
|
|
||||||
|
|
||||||
const url =
|
|
||||||
`https://accounts.google.com/o/oauth2/v2/auth` +
|
|
||||||
`?client_id=${encodeURIComponent(GOOGLE_CLIENT_ID)}` +
|
|
||||||
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
|
||||||
`&response_type=id_token` +
|
|
||||||
`&scope=${encodeURIComponent('openid email profile')}` +
|
|
||||||
`&nonce=${encodeURIComponent(nonce)}` +
|
|
||||||
`&state=${encodeURIComponent(state)}`
|
|
||||||
|
|
||||||
window.location.href = url
|
window.location.href = url
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function googleAuthWithToken(
|
export async function googleAuthWithToken(idToken, redirect_success, redirect_not_approved) {
|
||||||
idToken,
|
|
||||||
redirect_success,
|
|
||||||
redirect_not_approved,
|
|
||||||
options = {}, // { inviteToken?: string }
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
if (!idToken) {
|
|
||||||
toast.error('缺少 id_token')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
const payload = { idToken }
|
|
||||||
if (options && options.inviteToken) {
|
|
||||||
payload.inviteToken = options.inviteToken
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/google`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/google`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify({ idToken }),
|
||||||
})
|
})
|
||||||
|
const data = await res.json()
|
||||||
const data = await res.json().catch(() => ({}))
|
if (res.ok && data.token) {
|
||||||
|
|
||||||
if (res.ok && data && data.token) {
|
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
await loadCurrentUser()
|
await loadCurrentUser()
|
||||||
toast.success('登录成功')
|
toast.success('登录成功')
|
||||||
registerPush?.()
|
registerPush()
|
||||||
if (typeof redirect_success === 'function') redirect_success()
|
if (redirect_success) redirect_success()
|
||||||
return
|
} else if (data.reason_code === 'NOT_APPROVED') {
|
||||||
}
|
|
||||||
|
|
||||||
if (data && data.reason_code === 'NOT_APPROVED') {
|
|
||||||
toast.info('当前为注册审核模式,请填写注册理由')
|
toast.info('当前为注册审核模式,请填写注册理由')
|
||||||
if (typeof redirect_not_approved === 'function') redirect_not_approved(data.token)
|
if (redirect_not_approved) redirect_not_approved(data.token)
|
||||||
return
|
} else if (data.reason_code === 'IS_APPROVING') {
|
||||||
}
|
|
||||||
|
|
||||||
if (data && data.reason_code === 'IS_APPROVING') {
|
|
||||||
toast.info('您的注册理由正在审批中')
|
toast.info('您的注册理由正在审批中')
|
||||||
if (typeof redirect_success === 'function') redirect_success()
|
if (redirect_success) redirect_success()
|
||||||
return
|
|
||||||
}
|
}
|
||||||
toast.error(data?.message || '登录失败')
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
|
||||||
toast.error('登录失败')
|
toast.error('登录失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,8 +20,7 @@ async function generateCodeChallenge(codeVerifier) {
|
|||||||
.replace(/=+$/, '')
|
.replace(/=+$/, '')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 邀请码明文放入 state;同时生成 csrf 放入 state 并在回调校验
|
export async function twitterAuthorize(state = '') {
|
||||||
export async function twitterAuthorize(inviteToken = '') {
|
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
||||||
const TWITTER_CLIENT_ID = config.public.twitterClientId
|
const TWITTER_CLIENT_ID = config.public.twitterClientId
|
||||||
@@ -29,30 +28,17 @@ export async function twitterAuthorize(inviteToken = '') {
|
|||||||
toast.error('Twitter 登录不可用')
|
toast.error('Twitter 登录不可用')
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if (state === '') {
|
||||||
|
state = Math.random().toString(36).substring(2, 15)
|
||||||
|
}
|
||||||
const redirectUri = `${WEBSITE_BASE_URL}/twitter-callback`
|
const redirectUri = `${WEBSITE_BASE_URL}/twitter-callback`
|
||||||
|
|
||||||
// PKCE
|
|
||||||
const codeVerifier = generateCodeVerifier()
|
const codeVerifier = generateCodeVerifier()
|
||||||
sessionStorage.setItem('twitter_code_verifier', codeVerifier)
|
sessionStorage.setItem('twitter_code_verifier', codeVerifier)
|
||||||
const codeChallenge = await generateCodeChallenge(codeVerifier)
|
const codeChallenge = await generateCodeChallenge(codeVerifier)
|
||||||
|
|
||||||
// CSRF + 邀请码一起放入 state
|
|
||||||
const csrf = Math.random().toString(36).slice(2)
|
|
||||||
sessionStorage.setItem('twitter_csrf_state', csrf)
|
|
||||||
const state = new URLSearchParams({
|
|
||||||
csrf,
|
|
||||||
invite_token: inviteToken || '',
|
|
||||||
}).toString()
|
|
||||||
|
|
||||||
const url =
|
const url =
|
||||||
`https://x.com/i/oauth2/authorize?response_type=code&client_id=${encodeURIComponent(TWITTER_CLIENT_ID)}` +
|
`https://x.com/i/oauth2/authorize?response_type=code&client_id=${TWITTER_CLIENT_ID}` +
|
||||||
`&redirect_uri=${encodeURIComponent(redirectUri)}` +
|
`&redirect_uri=${encodeURIComponent(redirectUri)}&scope=tweet.read%20users.read` +
|
||||||
`&scope=${encodeURIComponent('tweet.read users.read')}` +
|
`&state=${state}&code_challenge=${codeChallenge}&code_challenge_method=S256`
|
||||||
`&state=${encodeURIComponent(state)}` +
|
|
||||||
`&code_challenge=${encodeURIComponent(codeChallenge)}` +
|
|
||||||
`&code_challenge_method=S256`
|
|
||||||
|
|
||||||
window.location.href = url
|
window.location.href = url
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -60,29 +46,8 @@ export async function twitterExchange(code, state, reason) {
|
|||||||
try {
|
try {
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
const API_BASE_URL = config.public.apiBaseUrl
|
||||||
|
|
||||||
// 取出并清理 PKCE/CSRF
|
|
||||||
const codeVerifier = sessionStorage.getItem('twitter_code_verifier')
|
const codeVerifier = sessionStorage.getItem('twitter_code_verifier')
|
||||||
sessionStorage.removeItem('twitter_code_verifier')
|
sessionStorage.removeItem('twitter_code_verifier')
|
||||||
|
|
||||||
const savedCsrf = sessionStorage.getItem('twitter_csrf_state')
|
|
||||||
sessionStorage.removeItem('twitter_csrf_state')
|
|
||||||
|
|
||||||
// 从 state 解析 csrf 与 invite_token
|
|
||||||
let parsedCsrf = ''
|
|
||||||
let inviteToken = ''
|
|
||||||
try {
|
|
||||||
const sp = new URLSearchParams(state || '')
|
|
||||||
parsedCsrf = sp.get('csrf') || ''
|
|
||||||
inviteToken = sp.get('invite_token') || sp.get('invitetoken') || ''
|
|
||||||
} catch {}
|
|
||||||
|
|
||||||
// 简单 CSRF 校验(存在才校验,避免误杀老会话)
|
|
||||||
if (savedCsrf && parsedCsrf && savedCsrf !== parsedCsrf) {
|
|
||||||
toast.error('登录状态校验失败,请重试')
|
|
||||||
return { success: false, needReason: false, error: 'state mismatch' }
|
|
||||||
}
|
|
||||||
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/twitter`, {
|
const res = await fetch(`${API_BASE_URL}/api/auth/twitter`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
@@ -92,10 +57,8 @@ export async function twitterExchange(code, state, reason) {
|
|||||||
reason,
|
reason,
|
||||||
state,
|
state,
|
||||||
codeVerifier,
|
codeVerifier,
|
||||||
inviteToken,
|
|
||||||
}),
|
}),
|
||||||
})
|
})
|
||||||
|
|
||||||
const data = await res.json()
|
const data = await res.json()
|
||||||
if (res.ok && data.token) {
|
if (res.ok && data.token) {
|
||||||
setToken(data.token)
|
setToken(data.token)
|
||||||
@@ -114,7 +77,6 @@ export async function twitterExchange(code, state, reason) {
|
|||||||
return { success: false, needReason: false, error: data.error || '登录失败' }
|
return { success: false, needReason: false, error: data.error || '登录失败' }
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(e)
|
|
||||||
toast.error('登录失败')
|
toast.error('登录失败')
|
||||||
return { success: false, needReason: false, error: '登录失败' }
|
return { success: false, needReason: false, error: '登录失败' }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import Vditor from 'vditor'
|
|||||||
import { getToken, authState } from './auth'
|
import { getToken, authState } from './auth'
|
||||||
import { searchUsers, fetchFollowings, fetchAdmins } from './user'
|
import { searchUsers, fetchFollowings, fetchAdmins } from './user'
|
||||||
import { tiebaEmoji } from './tiebaEmoji'
|
import { tiebaEmoji } from './tiebaEmoji'
|
||||||
import '~/assets/global.css'
|
|
||||||
|
|
||||||
export function getEditorTheme() {
|
export function getEditorTheme() {
|
||||||
return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'classic'
|
return document.documentElement.dataset.theme === 'dark' ? 'dark' : 'classic'
|
||||||
|
|||||||
Reference in New Issue
Block a user