Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,11 @@ public class OAuthController {
@Value("${justauth.type.discord.redirect-uri:}")
private String discordRedirectUri;

// Discord 登录灰度白名单:逗号分隔的 Discord user id。非空=只放行名单内 id,
// 其他人在回调处被弹回 /login?error=discord_canary;空=对所有人开放(GA 时清空即可)。
@Value("${auth.discord.allowlist:}")
private String discordAllowlist;

@Value("${AUTH_URL:http://localhost:3000}")
private String frontEndUrl;

Expand Down Expand Up @@ -82,6 +87,19 @@ private void requireConfigured(String provider, String clientId, String clientSe
}
}

// 灰度白名单判定:空名单=全开放;否则精确匹配某个 Discord user id。
private boolean discordAllowed(String discordUserId) {
if (discordAllowlist == null || discordAllowlist.isBlank()) {
return true;
}
for (String id : discordAllowlist.split(",")) {
if (id.trim().equals(discordUserId)) {
return true;
}
}
return false;
}

// 仅用于排查日志:redirect_uri 是公开信息,不含密钥。
private String redirectUriOf(String provider) {
return switch (provider) {
Expand Down Expand Up @@ -179,6 +197,13 @@ public void login(@PathVariable String provider,

if (authResponse.ok()) {
AuthUser authUser = (AuthUser) authResponse.getData();
// Discord 灰度:非白名单 id 在此弹回(换 token 已发生,但不建号/不登入)。
// 直连 /oauth/render/discord 绕过前端按钮的人也一并挡在这里。
if ("discord".equals(provider) && !discordAllowed(authUser.getUuid())) {
log.info("[OAuth] discord 灰度:uuid={} 不在白名单,拒绝登录", authUser.getUuid());
response.sendRedirect(frontEndUrl + "/login?error=discord_canary");
return;
}
LoginResponse loginResponse = authService.loginByProvider(provider, authUser);
// token 放 URL fragment(#token=),不进服务器日志/Referer;前端读入 localStorage
response.sendRedirect(frontEndUrl + "/#token=" + loginResponse.tokenValue());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,13 @@ public SendResult sendOtp(String pendingId, String email) {
s.lastSendAtNanos = now;
to = normalized;
}
// 本地/CI 没配 Resend key 时:验证码直接打到控制台,让贡献者不配 Resend 也能
// 跑通完整注册流(Django/Rails 的 console email backend 同款)。生产必配 key,
// isConfigured() 为 true,走不到这里;这行只在开发环境出现。
if (!emailService.isConfigured()) {
log.warn("[DEV-OTP] Resend 未配置,验证码只打印到控制台(生产不应出现此行): email={} code={}", to, code);
return SendResult.SENT;
}
String html = "<p>你的 InvolutionHell 注册验证码是:</p>"
+ "<p style=\"font-size:24px;font-weight:bold;letter-spacing:4px\">" + code + "</p>"
+ "<p>10 分钟内有效。如果不是你本人操作,忽略即可。</p>";
Expand Down
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ justauth.type.github.redirect-uri=${AUTH_URL:http://localhost:3000}/api/auth/cal
justauth.type.discord.client-id=${AUTH_DISCORD_ID_DEV:${AUTH_DISCORD_ID:}}
justauth.type.discord.client-secret=${AUTH_DISCORD_SECRET_DEV:${AUTH_DISCORD_SECRET:}}
justauth.type.discord.redirect-uri=${AUTH_URL:http://localhost:3000}/api/auth/callback/discord
# Discord 登录灰度白名单(逗号分隔的 Discord user id)。非空=只这些 id 能用 Discord 登录,
# 其他人在回调处被弹回 /login?error=discord_canary。空=对所有人开放,GA 时清空即可。
auth.discord.allowlist=${AUTH_DISCORD_ALLOWLIST:}

# JWT ?? (Temporarily Commented Out for JustAuth Migration)
# jwt.secret-key=${AUTH_SECRET:involutionhell-default-secret-key-32-chars-long}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import com.github.benmanes.caffeine.cache.Ticker;
Expand All @@ -20,6 +22,7 @@
import java.util.concurrent.atomic.AtomicInteger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

Expand All @@ -41,6 +44,12 @@ private static final class FakeTicker implements Ticker {
private final FakeTicker ticker = new FakeTicker();
private final RegistrationService svc = new RegistrationService(email, ticker);

// 默认按"已配置 Resend"跑正常发信路径;dev-fallback 用例单独覆盖为 false。
@BeforeEach
void resendConfigured() {
when(email.isConfigured()).thenReturn(true);
}

private PendingRegistration reg(String providerEmail) {
return new PendingRegistration("discord", "snow-1", "Nick", null, providerEmail, null);
}
Expand Down Expand Up @@ -133,6 +142,15 @@ void otpExpiresAfterTtl() {
assertThat(svc.verifyAndConsume(pid, "a@e.com", code)).isEmpty();
}

@Test
void devConsoleFallbackWhenResendUnconfigured() {
when(email.isConfigured()).thenReturn(false); // 覆盖 @BeforeEach:本地没配 Resend
String pid = svc.begin(reg("a@e.com"));
// 不真发信,但流程照走(返回 SENT),验证码只进日志——贡献者本地读控制台即可
assertThat(svc.sendOtp(pid, "a@e.com")).isEqualTo(SendResult.SENT);
verify(email, never()).sendHtml(anyString(), anyString(), anyString());
}

@Test
void invalidEmailRejected() {
String pid = svc.begin(reg("a@e.com"));
Expand Down