Compare commits

...

6 Commits

Author SHA1 Message Date
Tim
f3512c1184 feat: add Telegram authentication 2025-09-01 11:39:10 +08:00
Tim
a428f472f2 Merge pull request #809 from nagisa77/codex/shorten-invitation-link
feat: shorten invite links
2025-09-01 11:26:25 +08:00
Tim
8544803e62 feat: shorten invite links 2025-09-01 11:25:32 +08:00
Tim
54874cea7a Merge pull request #808 from nagisa77/codex/add-email-notification-settings
feat: add email notification settings
2025-09-01 11:24:19 +08:00
Tim
098d82a6a0 feat: add email notification settings 2025-09-01 11:23:31 +08:00
Tim
90eee03198 Merge pull request #807 from nagisa77/codex/fix-backend-compilation-issues
test: fix PostServiceTest for new PostService deps
2025-09-01 10:54:07 +08:00
20 changed files with 454 additions and 9 deletions

View File

@@ -26,6 +26,7 @@ public class AuthController {
private final GithubAuthService githubAuthService;
private final DiscordAuthService discordAuthService;
private final TwitterAuthService twitterAuthService;
private final TelegramAuthService telegramAuthService;
private final RegisterModeService registerModeService;
private final NotificationService notificationService;
private final UserRepository userRepository;
@@ -360,6 +361,51 @@ 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")
public ResponseEntity<?> checkToken() {
return ResponseEntity.ok(Map.of("valid", true));

View File

@@ -62,4 +62,14 @@ public class NotificationController {
public void updatePref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updatePreference(auth.getName(), req.getType(), req.isEnabled());
}
@GetMapping("/email-prefs")
public List<NotificationPreferenceDto> emailPrefs(Authentication auth) {
return notificationService.listEmailPreferences(auth.getName());
}
@PostMapping("/email-prefs")
public void updateEmailPref(@RequestBody NotificationPreferenceUpdateRequest req, Authentication auth) {
notificationService.updateEmailPreference(auth.getName(), req.getType(), req.isEnabled());
}
}

View File

@@ -0,0 +1,16 @@
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;
}

View File

@@ -14,6 +14,13 @@ public class InviteToken {
@Id
private String token;
/**
* Short token used in invite links. Existing records may have this field null
* and fall back to {@link #token} for backward compatibility.
*/
@Column(unique = true)
private String shortToken;
@ManyToOne
private User inviter;

View File

@@ -74,6 +74,12 @@ public class User {
NotificationType.USER_ACTIVITY
);
@ElementCollection(targetClass = NotificationType.class)
@CollectionTable(name = "user_disabled_email_notification_types", joinColumns = @JoinColumn(name = "user_id"))
@Column(name = "notification_type")
@Enumerated(EnumType.STRING)
private Set<NotificationType> disabledEmailNotificationTypes = EnumSet.noneOf(NotificationType.class);
@CreationTimestamp
@Column(nullable = false, updatable = false,
columnDefinition = "DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)")

View File

@@ -9,4 +9,8 @@ import java.util.Optional;
public interface InviteTokenRepository extends JpaRepository<InviteToken, String> {
Optional<InviteToken> findByInviterAndCreatedDate(User inviter, LocalDate createdDate);
Optional<InviteToken> findByShortToken(String shortToken);
boolean existsByShortToken(String shortToken);
}

View File

@@ -30,33 +30,53 @@ public class InviteService {
LocalDate today = LocalDate.now();
Optional<InviteToken> existing = inviteTokenRepository.findByInviterAndCreatedDate(inviter, today);
if (existing.isPresent()) {
return existing.get().getToken();
InviteToken inviteToken = existing.get();
return inviteToken.getShortToken() != null ? inviteToken.getShortToken() : inviteToken.getToken();
}
String token = jwtService.generateInviteToken(username);
String shortToken;
do {
shortToken = java.util.UUID.randomUUID().toString().replace("-", "").substring(0, 8);
} while (inviteTokenRepository.existsByShortToken(shortToken));
InviteToken inviteToken = new InviteToken();
inviteToken.setToken(token);
inviteToken.setShortToken(shortToken);
inviteToken.setInviter(inviter);
inviteToken.setCreatedDate(today);
inviteToken.setUsageCount(0);
inviteTokenRepository.save(inviteToken);
return token;
return shortToken;
}
public InviteValidateResult validate(String token) {
if (token == null || token.isEmpty()) {
return new InviteValidateResult(null, false);
}
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
String realToken = token;
if (invite == null) {
invite = inviteTokenRepository.findByShortToken(token).orElse(null);
if (invite == null) {
return new InviteValidateResult(null, false);
}
realToken = invite.getToken();
}
try {
jwtService.validateAndGetSubjectForInvite(token);
jwtService.validateAndGetSubjectForInvite(realToken);
} catch (Exception e) {
return new InviteValidateResult(null, false);
}
InviteToken invite = inviteTokenRepository.findById(token).orElse(null);
return new InviteValidateResult(invite, invite != null && invite.getUsageCount() < 3);
return new InviteValidateResult(invite, invite.getUsageCount() < 3);
}
public void consume(String token, String newUserName) {
InviteToken invite = inviteTokenRepository.findById(token).orElseThrow();
InviteToken invite = inviteTokenRepository.findById(token)
.orElseGet(() -> inviteTokenRepository.findByShortToken(token).orElseThrow());
invite.setUsageCount(invite.getUsageCount() + 1);
inviteTokenRepository.save(invite);
pointService.awardForInvite(invite.getInviter().getUsername(), newUserName);

View File

@@ -19,6 +19,7 @@ import java.util.regex.Pattern;
import java.util.regex.Matcher;
import java.util.Set;
import java.util.HashSet;
import java.util.EnumSet;
import java.util.List;
import java.util.ArrayList;
@@ -40,6 +41,12 @@ public class NotificationService {
private static final Pattern MENTION_PATTERN = Pattern.compile("@\\[([^\\]]+)\\]");
private static final Set<NotificationType> EMAIL_TYPES = EnumSet.of(
NotificationType.COMMENT_REPLY,
NotificationType.LOTTERY_WIN,
NotificationType.LOTTERY_DRAW
);
private String buildPayload(String body, String url) {
// Ensure push notifications contain a link to the related resource so
// that verifications can assert its presence and users can navigate
@@ -75,7 +82,8 @@ public class NotificationService {
n = notificationRepository.save(n);
// Runnable asyncTask = () -> {
if (type == NotificationType.COMMENT_REPLY && user.getEmail() != null && post != null && comment != null) {
if (type == NotificationType.COMMENT_REPLY && user.getEmail() != null && post != null && comment != null
&& !user.getDisabledEmailNotificationTypes().contains(NotificationType.COMMENT_REPLY)) {
String url = String.format("%s/posts/%d#comment-%d", websiteUrl, post.getId(), comment.getId());
emailSender.sendEmail(user.getEmail(), "有人回复了你", url);
sendCustomPush(user, "有人回复了你", url);
@@ -187,6 +195,35 @@ public class NotificationService {
userRepository.save(user);
}
public List<NotificationPreferenceDto> listEmailPreferences(String username) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
Set<NotificationType> disabled = user.getDisabledEmailNotificationTypes();
List<NotificationPreferenceDto> prefs = new ArrayList<>();
for (NotificationType nt : EMAIL_TYPES) {
NotificationPreferenceDto dto = new NotificationPreferenceDto();
dto.setType(nt);
dto.setEnabled(!disabled.contains(nt));
prefs.add(dto);
}
return prefs;
}
public void updateEmailPreference(String username, NotificationType type, boolean enabled) {
if (!EMAIL_TYPES.contains(type)) {
return;
}
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));
Set<NotificationType> disabled = user.getDisabledEmailNotificationTypes();
if (enabled) {
disabled.remove(type);
} else {
disabled.add(type);
}
userRepository.save(user);
}
public List<Notification> listNotifications(String username, Boolean read, int page, int size) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new com.openisle.exception.NotFoundException("User not found"));

View File

@@ -374,14 +374,16 @@ public class PostService {
lp.setWinners(winners);
lotteryPostRepository.save(lp);
for (User w : winners) {
if (w.getEmail() != null) {
if (w.getEmail() != null &&
!w.getDisabledEmailNotificationTypes().contains(NotificationType.LOTTERY_WIN)) {
emailSender.sendEmail(w.getEmail(), "你中奖了", "恭喜你在抽奖贴 \"" + lp.getTitle() + "\" 中获奖");
}
notificationService.createNotification(w, NotificationType.LOTTERY_WIN, lp, null, null, lp.getAuthor(), null, null);
notificationService.sendCustomPush(w, "你中奖了", String.format("%s/posts/%d", websiteUrl, lp.getId()));
}
if (lp.getAuthor() != null) {
if (lp.getAuthor().getEmail() != null) {
if (lp.getAuthor().getEmail() != null &&
!lp.getAuthor().getDisabledEmailNotificationTypes().contains(NotificationType.LOTTERY_DRAW)) {
emailSender.sendEmail(lp.getAuthor().getEmail(), "抽奖已开奖", "您的抽奖贴 \"" + lp.getTitle() + "\" 已开奖");
}
notificationService.createNotification(lp.getAuthor(), NotificationType.LOTTERY_DRAW, lp, null, null, null, null, null);

View File

@@ -0,0 +1,102 @@
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);
}
}

View File

@@ -69,6 +69,8 @@ discord.client-secret=${DISCORD_CLIENT_SECRET:}
# Twitter OAuth configuration
twitter.client-id=${TWITTER_CLIENT_ID:}
twitter.client-secret=${TWITTER_CLIENT_SECRET:}
# Telegram login configuration
telegram.bot-token=${TELEGRAM_BOT_TOKEN:}
# OpenAI configuration
openai.api-key=${OPENAI_API_KEY:}
openai.model=${OPENAI_MODEL:gpt-4o}

View File

@@ -58,6 +58,7 @@ const hideMenu = computed(() => {
'/discord-callback',
'/forgot-password',
'/google-callback',
'/telegram-callback',
].includes(useRoute().path)
})

View File

@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 438 B

View File

@@ -11,6 +11,7 @@ export default defineNuxtConfig({
githubClientId: process.env.NUXT_PUBLIC_GITHUB_CLIENT_ID || '',
discordClientId: process.env.NUXT_PUBLIC_DISCORD_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'],

View File

@@ -51,6 +51,14 @@
<img class="login-page-button-icon" src="../assets/icons/twitter.svg" alt="Twitter Logo" />
<div class="login-page-button-text">Twitter 登录</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>
</template>
@@ -62,6 +70,7 @@ import { googleAuthorize } from '~/utils/google'
import { githubAuthorize } from '~/utils/github'
import { discordAuthorize } from '~/utils/discord'
import { twitterAuthorize } from '~/utils/twitter'
import { telegramAuthorize } from '~/utils/telegram'
import BaseInput from '~/components/BaseInput.vue'
import { registerPush } from '~/utils/push'
const config = useRuntimeConfig()
@@ -118,6 +127,9 @@ const loginWithDiscord = () => {
const loginWithTwitter = () => {
twitterAuthorize()
}
const loginWithTelegram = () => {
telegramAuthorize()
}
</script>
<style scoped>

View File

@@ -23,6 +23,18 @@
</div>
</div>
</div>
<div class="message-control-container">
<div class="message-control-title">邮件通知设置</div>
<div class="message-control-item-container">
<div v-for="pref in emailPrefs" :key="pref.type" class="message-control-item">
<div class="message-control-item-label">{{ formatType(pref.type) }}</div>
<BaseSwitch
:model-value="pref.enabled"
@update:modelValue="(val) => toggleEmailPref(pref, val)"
/>
</div>
</div>
</div>
</div>
<template v-else>
@@ -579,6 +591,8 @@ import {
hasMore,
fetchNotificationPreferences,
updateNotificationPreference,
fetchEmailNotificationPreferences,
updateEmailNotificationPreference,
} from '~/utils/notification'
import TimeManager from '~/utils/time'
import BaseSwitch from '~/components/BaseSwitch.vue'
@@ -595,6 +609,7 @@ const tabs = [
{ key: 'control', label: '消息设置' },
]
const notificationPrefs = ref([])
const emailPrefs = ref([])
const page = ref(0)
const pageSize = 30
@@ -619,6 +634,10 @@ const fetchPrefs = async () => {
notificationPrefs.value = await fetchNotificationPreferences()
}
const fetchEmailPrefs = async () => {
emailPrefs.value = await fetchEmailNotificationPreferences()
}
const togglePref = async (pref, value) => {
const ok = await updateNotificationPreference(pref.type, value)
if (ok) {
@@ -634,6 +653,15 @@ const togglePref = async (pref, value) => {
}
}
const toggleEmailPref = async (pref, value) => {
const ok = await updateEmailNotificationPreference(pref.type, value)
if (ok) {
pref.enabled = value
} else {
toast.error('操作失败')
}
}
const markRead = async (id) => {
markNotificationRead(id)
if (selectedTab.value === 'unread') {
@@ -729,6 +757,7 @@ onActivated(async () => {
page.value = 0
await fetchNotifications({ page: 0, size: pageSize, unread: selectedTab.value === 'unread' })
fetchPrefs()
fetchEmailPrefs()
})
</script>

View File

@@ -85,6 +85,14 @@
<img class="signup-page-button-icon" src="~/assets/icons/twitter.svg" alt="Twitter Logo" />
<div class="signup-page-button-text">Twitter 注册</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>
</template>
@@ -96,6 +104,7 @@ import { discordAuthorize } from '~/utils/discord'
import { githubAuthorize } from '~/utils/github'
import { googleAuthorize } from '~/utils/google'
import { twitterAuthorize } from '~/utils/twitter'
import { telegramAuthorize } from '~/utils/telegram'
import { loadCurrentUser, setToken } from '~/utils/auth'
const route = useRoute()
@@ -228,6 +237,9 @@ const signupWithDiscord = () => {
const signupWithTwitter = () => {
twitterAuthorize(inviteToken.value)
}
const signupWithTelegram = () => {
telegramAuthorize(inviteToken.value)
}
</script>
<style scoped>

View File

@@ -0,0 +1,41 @@
<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>

View File

@@ -116,6 +116,43 @@ export async function updateNotificationPreference(type, enabled) {
}
}
export async function fetchEmailNotificationPreferences() {
try {
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const token = getToken()
if (!token) return []
const res = await fetch(`${API_BASE_URL}/api/notifications/email-prefs`, {
headers: { Authorization: `Bearer ${token}` },
})
if (!res.ok) return []
return await res.json()
} catch (e) {
return []
}
}
export async function updateEmailNotificationPreference(type, enabled) {
try {
const config = useRuntimeConfig()
const API_BASE_URL = config.public.apiBaseUrl
const token = getToken()
if (!token) return false
const res = await fetch(`${API_BASE_URL}/api/notifications/email-prefs`, {
method: 'POST',
headers: {
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ type, enabled }),
})
return res.ok
} catch (e) {
return false
}
}
/**
* 处理信息的高阶函数
* @returns

View File

@@ -0,0 +1,56 @@
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: '登录失败' }
}
}