mirror of
https://github.com/nagisa77/OpenIsle.git
synced 2026-03-04 11:00:47 +08:00
Compare commits
1 Commits
codex/supp
...
codex/upda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
27c217a630 |
@@ -26,7 +26,6 @@ public class AuthController {
|
|||||||
private final GithubAuthService githubAuthService;
|
private final GithubAuthService githubAuthService;
|
||||||
private final DiscordAuthService discordAuthService;
|
private final DiscordAuthService discordAuthService;
|
||||||
private final TwitterAuthService twitterAuthService;
|
private final TwitterAuthService twitterAuthService;
|
||||||
private final TelegramAuthService telegramAuthService;
|
|
||||||
private final RegisterModeService registerModeService;
|
private final RegisterModeService registerModeService;
|
||||||
private final NotificationService notificationService;
|
private final NotificationService notificationService;
|
||||||
private final UserRepository userRepository;
|
private final UserRepository userRepository;
|
||||||
@@ -361,51 +360,6 @@ public class AuthController {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/telegram")
|
|
||||||
public ResponseEntity<?> loginWithTelegram(@RequestBody TelegramLoginRequest req) {
|
|
||||||
boolean viaInvite = req.getInviteToken() != null && !req.getInviteToken().isEmpty();
|
|
||||||
InviteService.InviteValidateResult inviteValidateResult = inviteService.validate(req.getInviteToken());
|
|
||||||
if (viaInvite && !inviteValidateResult.isValidate()) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of("error", "Invalid invite token"));
|
|
||||||
}
|
|
||||||
Optional<AuthResult> resultOpt = telegramAuthService.authenticate(
|
|
||||||
req,
|
|
||||||
registerModeService.getRegisterMode(),
|
|
||||||
viaInvite);
|
|
||||||
if (resultOpt.isPresent()) {
|
|
||||||
AuthResult result = resultOpt.get();
|
|
||||||
if (viaInvite && result.isNewUser()) {
|
|
||||||
inviteService.consume(req.getInviteToken(), inviteValidateResult.getInviteToken().getInviter().getUsername());
|
|
||||||
return ResponseEntity.ok(Map.of(
|
|
||||||
"token", jwtService.generateToken(result.getUser().getUsername()),
|
|
||||||
"reason_code", "INVITE_APPROVED"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
if (RegisterMode.DIRECT.equals(registerModeService.getRegisterMode())) {
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
|
||||||
}
|
|
||||||
if (!result.getUser().isApproved()) {
|
|
||||||
if (result.getUser().getRegisterReason() != null && !result.getUser().getRegisterReason().isEmpty()) {
|
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
|
||||||
"error", "Account awaiting approval",
|
|
||||||
"reason_code", "IS_APPROVING",
|
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
|
||||||
"error", "Account awaiting approval",
|
|
||||||
"reason_code", "NOT_APPROVED",
|
|
||||||
"token", jwtService.generateReasonToken(result.getUser().getUsername())
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return ResponseEntity.ok(Map.of("token", jwtService.generateToken(result.getUser().getUsername())));
|
|
||||||
}
|
|
||||||
return ResponseEntity.badRequest().body(Map.of(
|
|
||||||
"error", "Invalid telegram data",
|
|
||||||
"reason_code", "INVALID_CREDENTIALS"
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
@GetMapping("/check")
|
@GetMapping("/check")
|
||||||
public ResponseEntity<?> checkToken() {
|
public ResponseEntity<?> checkToken() {
|
||||||
return ResponseEntity.ok(Map.of("valid", true));
|
return ResponseEntity.ok(Map.of("valid", true));
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
package com.openisle.dto;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
/** Request for Telegram login. */
|
|
||||||
@Data
|
|
||||||
public class TelegramLoginRequest {
|
|
||||||
private String id;
|
|
||||||
private String firstName;
|
|
||||||
private String lastName;
|
|
||||||
private String username;
|
|
||||||
private String photoUrl;
|
|
||||||
private Long authDate;
|
|
||||||
private String hash;
|
|
||||||
private String inviteToken;
|
|
||||||
}
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
package com.openisle.service;
|
|
||||||
|
|
||||||
import com.openisle.dto.TelegramLoginRequest;
|
|
||||||
import com.openisle.model.RegisterMode;
|
|
||||||
import com.openisle.model.Role;
|
|
||||||
import com.openisle.model.User;
|
|
||||||
import com.openisle.repository.UserRepository;
|
|
||||||
import lombok.RequiredArgsConstructor;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import javax.crypto.Mac;
|
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
|
||||||
import java.nio.charset.StandardCharsets;
|
|
||||||
import java.security.MessageDigest;
|
|
||||||
import java.util.*;
|
|
||||||
|
|
||||||
@Service
|
|
||||||
@RequiredArgsConstructor
|
|
||||||
public class TelegramAuthService {
|
|
||||||
private final UserRepository userRepository;
|
|
||||||
private final AvatarGenerator avatarGenerator;
|
|
||||||
|
|
||||||
@Value("${telegram.bot-token:}")
|
|
||||||
private String botToken;
|
|
||||||
|
|
||||||
public Optional<AuthResult> authenticate(TelegramLoginRequest req, RegisterMode mode, boolean viaInvite) {
|
|
||||||
try {
|
|
||||||
if (botToken == null || botToken.isEmpty()) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
String dataCheckString = buildDataCheckString(req);
|
|
||||||
MessageDigest md = MessageDigest.getInstance("SHA-256");
|
|
||||||
byte[] secretKey = md.digest(botToken.getBytes(StandardCharsets.UTF_8));
|
|
||||||
Mac mac = Mac.getInstance("HmacSHA256");
|
|
||||||
mac.init(new SecretKeySpec(secretKey, "HmacSHA256"));
|
|
||||||
byte[] hash = mac.doFinal(dataCheckString.getBytes(StandardCharsets.UTF_8));
|
|
||||||
String hex = bytesToHex(hash);
|
|
||||||
if (!hex.equalsIgnoreCase(req.getHash())) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
String username = req.getUsername();
|
|
||||||
String email = (username != null ? username : req.getId()) + "@telegram.org";
|
|
||||||
String avatar = req.getPhotoUrl();
|
|
||||||
return Optional.of(processUser(email, username, avatar, mode, viaInvite));
|
|
||||||
} catch (Exception e) {
|
|
||||||
return Optional.empty();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private String buildDataCheckString(TelegramLoginRequest req) {
|
|
||||||
List<String> data = new ArrayList<>();
|
|
||||||
if (req.getAuthDate() != null) data.add("auth_date=" + req.getAuthDate());
|
|
||||||
if (req.getFirstName() != null) data.add("first_name=" + req.getFirstName());
|
|
||||||
if (req.getId() != null) data.add("id=" + req.getId());
|
|
||||||
if (req.getLastName() != null) data.add("last_name=" + req.getLastName());
|
|
||||||
if (req.getPhotoUrl() != null) data.add("photo_url=" + req.getPhotoUrl());
|
|
||||||
if (req.getUsername() != null) data.add("username=" + req.getUsername());
|
|
||||||
Collections.sort(data);
|
|
||||||
return String.join("\n", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private String bytesToHex(byte[] bytes) {
|
|
||||||
StringBuilder sb = new StringBuilder();
|
|
||||||
for (byte b : bytes) {
|
|
||||||
sb.append(String.format("%02x", b));
|
|
||||||
}
|
|
||||||
return sb.toString();
|
|
||||||
}
|
|
||||||
|
|
||||||
private AuthResult processUser(String email, String username, String avatar, RegisterMode mode, boolean viaInvite) {
|
|
||||||
Optional<User> existing = userRepository.findByEmail(email);
|
|
||||||
if (existing.isPresent()) {
|
|
||||||
User user = existing.get();
|
|
||||||
if (!user.isVerified()) {
|
|
||||||
user.setVerified(true);
|
|
||||||
user.setVerificationCode(null);
|
|
||||||
userRepository.save(user);
|
|
||||||
}
|
|
||||||
return new AuthResult(user, false);
|
|
||||||
}
|
|
||||||
String baseUsername = username != null ? username : email.split("@")[0];
|
|
||||||
String finalUsername = baseUsername;
|
|
||||||
int suffix = 1;
|
|
||||||
while (userRepository.findByUsername(finalUsername).isPresent()) {
|
|
||||||
finalUsername = baseUsername + suffix++;
|
|
||||||
}
|
|
||||||
User user = new User();
|
|
||||||
user.setUsername(finalUsername);
|
|
||||||
user.setEmail(email);
|
|
||||||
user.setPassword("");
|
|
||||||
user.setRole(Role.USER);
|
|
||||||
user.setVerified(true);
|
|
||||||
user.setApproved(mode == RegisterMode.DIRECT || viaInvite);
|
|
||||||
if (avatar != null) {
|
|
||||||
user.setAvatar(avatar);
|
|
||||||
} else {
|
|
||||||
user.setAvatar(avatarGenerator.generate(finalUsername));
|
|
||||||
}
|
|
||||||
return new AuthResult(userRepository.save(user), true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -69,8 +69,6 @@ discord.client-secret=${DISCORD_CLIENT_SECRET:}
|
|||||||
# Twitter OAuth configuration
|
# Twitter OAuth configuration
|
||||||
twitter.client-id=${TWITTER_CLIENT_ID:}
|
twitter.client-id=${TWITTER_CLIENT_ID:}
|
||||||
twitter.client-secret=${TWITTER_CLIENT_SECRET:}
|
twitter.client-secret=${TWITTER_CLIENT_SECRET:}
|
||||||
# Telegram login configuration
|
|
||||||
telegram.bot-token=${TELEGRAM_BOT_TOKEN:}
|
|
||||||
# OpenAI configuration
|
# OpenAI configuration
|
||||||
openai.api-key=${OPENAI_API_KEY:}
|
openai.api-key=${OPENAI_API_KEY:}
|
||||||
openai.model=${OPENAI_MODEL:gpt-4o}
|
openai.model=${OPENAI_MODEL:gpt-4o}
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ const hideMenu = computed(() => {
|
|||||||
'/discord-callback',
|
'/discord-callback',
|
||||||
'/forgot-password',
|
'/forgot-password',
|
||||||
'/google-callback',
|
'/google-callback',
|
||||||
'/telegram-callback',
|
|
||||||
].includes(useRoute().path)
|
].includes(useRoute().path)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
|
||||||
<path fill="#2AABEE" d="M12 0C5.372 0 0 5.372 0 12s5.372 12 12 12 12-5.372 12-12S18.628 0 12 0z"/>
|
|
||||||
<path fill="#fff" d="M17.565 7.06L15.7 17.05c-.14.706-.51.88-1.033.548l-2.861-2.108-1.382 1.332c-.153.153-.282.282-.575.282l.205-2.912 5.303-4.788c.231-.205-.05-.32-.36-.116L8.9 11.27l-3.14-.98c-.682-.213-.696-.682.143-1.007l11.18-4.307c.511-.186.958.116.783.914z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 438 B |
@@ -11,7 +11,6 @@ export default defineNuxtConfig({
|
|||||||
githubClientId: process.env.NUXT_PUBLIC_GITHUB_CLIENT_ID || '',
|
githubClientId: process.env.NUXT_PUBLIC_GITHUB_CLIENT_ID || '',
|
||||||
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '',
|
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '',
|
||||||
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
twitterClientId: process.env.NUXT_PUBLIC_TWITTER_CLIENT_ID || '',
|
||||||
telegramBotId: process.env.NUXT_PUBLIC_TELEGRAM_BOT_ID || '',
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
|
css: ['vditor/dist/index.css', '~/assets/fonts.css', '~/assets/global.css'],
|
||||||
|
|||||||
@@ -51,14 +51,6 @@
|
|||||||
<img class="login-page-button-icon" src="../assets/icons/twitter.svg" alt="Twitter Logo" />
|
<img class="login-page-button-icon" src="../assets/icons/twitter.svg" alt="Twitter Logo" />
|
||||||
<div class="login-page-button-text">Twitter 登录</div>
|
<div class="login-page-button-text">Twitter 登录</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="login-page-button" @click="loginWithTelegram">
|
|
||||||
<img
|
|
||||||
class="login-page-button-icon"
|
|
||||||
src="../assets/icons/telegram.svg"
|
|
||||||
alt="Telegram Logo"
|
|
||||||
/>
|
|
||||||
<div class="login-page-button-text">Telegram 登录</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -70,7 +62,6 @@ import { googleAuthorize } from '~/utils/google'
|
|||||||
import { githubAuthorize } from '~/utils/github'
|
import { githubAuthorize } from '~/utils/github'
|
||||||
import { discordAuthorize } from '~/utils/discord'
|
import { discordAuthorize } from '~/utils/discord'
|
||||||
import { twitterAuthorize } from '~/utils/twitter'
|
import { twitterAuthorize } from '~/utils/twitter'
|
||||||
import { telegramAuthorize } from '~/utils/telegram'
|
|
||||||
import BaseInput from '~/components/BaseInput.vue'
|
import BaseInput from '~/components/BaseInput.vue'
|
||||||
import { registerPush } from '~/utils/push'
|
import { registerPush } from '~/utils/push'
|
||||||
const config = useRuntimeConfig()
|
const config = useRuntimeConfig()
|
||||||
@@ -127,9 +118,6 @@ const loginWithDiscord = () => {
|
|||||||
const loginWithTwitter = () => {
|
const loginWithTwitter = () => {
|
||||||
twitterAuthorize()
|
twitterAuthorize()
|
||||||
}
|
}
|
||||||
const loginWithTelegram = () => {
|
|
||||||
telegramAuthorize()
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -85,14 +85,6 @@
|
|||||||
<img class="signup-page-button-icon" src="~/assets/icons/twitter.svg" alt="Twitter Logo" />
|
<img class="signup-page-button-icon" src="~/assets/icons/twitter.svg" alt="Twitter Logo" />
|
||||||
<div class="signup-page-button-text">Twitter 注册</div>
|
<div class="signup-page-button-text">Twitter 注册</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="signup-page-button" @click="signupWithTelegram">
|
|
||||||
<img
|
|
||||||
class="signup-page-button-icon"
|
|
||||||
src="~/assets/icons/telegram.svg"
|
|
||||||
alt="Telegram Logo"
|
|
||||||
/>
|
|
||||||
<div class="signup-page-button-text">Telegram 注册</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -104,7 +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 { telegramAuthorize } from '~/utils/telegram'
|
|
||||||
import { loadCurrentUser, setToken } from '~/utils/auth'
|
import { loadCurrentUser, setToken } from '~/utils/auth'
|
||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@@ -237,9 +228,6 @@ const signupWithDiscord = () => {
|
|||||||
const signupWithTwitter = () => {
|
const signupWithTwitter = () => {
|
||||||
twitterAuthorize(inviteToken.value)
|
twitterAuthorize(inviteToken.value)
|
||||||
}
|
}
|
||||||
const signupWithTelegram = () => {
|
|
||||||
telegramAuthorize(inviteToken.value)
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|||||||
@@ -1,41 +0,0 @@
|
|||||||
<template>
|
|
||||||
<CallbackPage />
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import CallbackPage from '~/components/CallbackPage.vue'
|
|
||||||
import { telegramExchange } from '~/utils/telegram'
|
|
||||||
|
|
||||||
onMounted(async () => {
|
|
||||||
const url = new URL(window.location.href)
|
|
||||||
const inviteToken =
|
|
||||||
url.searchParams.get('invite_token') || url.searchParams.get('invitetoken') || ''
|
|
||||||
const hash = url.hash.startsWith('#tgAuthResult=') ? url.hash.slice('#tgAuthResult='.length) : ''
|
|
||||||
if (!hash) {
|
|
||||||
navigateTo('/login', { replace: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
let authData
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(decodeURIComponent(hash))
|
|
||||||
authData = {
|
|
||||||
id: String(parsed.id),
|
|
||||||
firstName: parsed.first_name,
|
|
||||||
lastName: parsed.last_name,
|
|
||||||
username: parsed.username,
|
|
||||||
photoUrl: parsed.photo_url,
|
|
||||||
authDate: parsed.auth_date,
|
|
||||||
hash: parsed.hash,
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
navigateTo('/login', { replace: true })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const result = await telegramExchange(authData, inviteToken, '')
|
|
||||||
if (result.needReason) {
|
|
||||||
navigateTo(`/signup-reason?token=${result.token}`, { replace: true })
|
|
||||||
} else {
|
|
||||||
navigateTo('/', { replace: true })
|
|
||||||
}
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
@@ -58,7 +58,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="profile-info-item">
|
<div class="profile-info-item">
|
||||||
<div class="profile-info-item-label">最后发帖时间:</div>
|
<div class="profile-info-item-label">最后发帖时间:</div>
|
||||||
<div class="profile-info-item-value">{{ formatDate(user.lastPostTime) }}</div>
|
<div class="profile-info-item-value">
|
||||||
|
{{ user.lastPostTime != null ? formatDate(user.lastPostTime) : '暂无帖子' }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="profile-info-item">
|
<div class="profile-info-item">
|
||||||
<div class="profile-info-item-label">最后评论时间:</div>
|
<div class="profile-info-item-label">最后评论时间:</div>
|
||||||
|
|||||||
@@ -1,56 +0,0 @@
|
|||||||
import { toast } from '../main'
|
|
||||||
import { setToken, loadCurrentUser } from './auth'
|
|
||||||
import { registerPush } from './push'
|
|
||||||
|
|
||||||
export function telegramAuthorize(inviteToken = '') {
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const WEBSITE_BASE_URL = config.public.websiteBaseUrl
|
|
||||||
const TELEGRAM_BOT_ID = config.public.telegramBotId
|
|
||||||
if (!TELEGRAM_BOT_ID) {
|
|
||||||
toast.error('Telegram 登录不可用')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
const redirectUri = `${WEBSITE_BASE_URL}/telegram-callback${inviteToken ? `?invite_token=${encodeURIComponent(inviteToken)}` : ''}`
|
|
||||||
const url =
|
|
||||||
`https://oauth.telegram.org/auth` +
|
|
||||||
`?bot_id=${encodeURIComponent(TELEGRAM_BOT_ID)}` +
|
|
||||||
`&origin=${encodeURIComponent(WEBSITE_BASE_URL)}` +
|
|
||||||
`&request_access=write` +
|
|
||||||
`&redirect_uri=${encodeURIComponent(redirectUri)}`
|
|
||||||
window.location.href = url
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function telegramExchange(authData, inviteToken = '', reason = '') {
|
|
||||||
try {
|
|
||||||
const config = useRuntimeConfig()
|
|
||||||
const API_BASE_URL = config.public.apiBaseUrl
|
|
||||||
const payload = { ...authData, reason }
|
|
||||||
if (inviteToken) payload.inviteToken = inviteToken
|
|
||||||
const res = await fetch(`${API_BASE_URL}/api/auth/telegram`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload),
|
|
||||||
})
|
|
||||||
const data = await res.json()
|
|
||||||
if (res.ok && data.token) {
|
|
||||||
setToken(data.token)
|
|
||||||
await loadCurrentUser()
|
|
||||||
toast.success('登录成功')
|
|
||||||
registerPush?.()
|
|
||||||
return { success: true, needReason: false }
|
|
||||||
} else if (data.reason_code === 'NOT_APPROVED') {
|
|
||||||
toast.info('当前为注册审核模式,请填写注册理由')
|
|
||||||
return { success: false, needReason: true, token: data.token }
|
|
||||||
} else if (data.reason_code === 'IS_APPROVING') {
|
|
||||||
toast.info('您的注册理由正在审批中')
|
|
||||||
return { success: true, needReason: false }
|
|
||||||
} else {
|
|
||||||
toast.error(data.error || '登录失败')
|
|
||||||
return { success: false, needReason: false, error: data.error || '登录失败' }
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e)
|
|
||||||
toast.error('登录失败')
|
|
||||||
return { success: false, needReason: false, error: '登录失败' }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user