新增 LinuxDo Oauth2 登录

This commit is contained in:
eoao
2025-11-08 21:33:00 +08:00
parent c774009f82
commit d611436115
19 changed files with 451 additions and 36 deletions
+3 -2
View File
@@ -4,8 +4,9 @@
<meta charset="UTF-8"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0"/>
<meta name="theme-color" content="#F1F1F1" id="theme-color-meta">
<title></title>
<link rel="icon" href="./public/mail.png" type="image/png">
<meta name="description" content="Cloud Mail 是一个 Serverless 响应式邮箱服务,支持邮件发送,可部署到 Cloudflare 平台,降低服务器成本。Cloud Mail is a serverless responsive email service that supports email sending and can be deployed on Cloudflare to reduce server costs.">
<title>Cloud Mail</title>
<link rel="icon" href="/public/mail.png" type="image/png">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600&display=swap" rel="stylesheet">
<script src="https://challenges.cloudflare.com/turnstile/v0/api.js" async defer></script>
<script>
+2
View File
@@ -2,3 +2,5 @@
Cache-Control: public, max-age=31556952, immutable
/tinymce/*
Cache-Control: public, max-age=604800, immutable
/image/*
Cache-Control: public, max-age=604800, immutable
Binary file not shown.

After

Width:  |  Height:  |  Size: 622 B

+9
View File
@@ -0,0 +1,9 @@
import http from '@/axios/index.js';
export function oauthLinuxDoLogin(code) {
return http.post('/oauth/linuxDo/login',{code})
}
export function oauthBindUser(form) {
return http.put('/oauth/bindUser', form)
}
+199 -13
View File
@@ -1,5 +1,5 @@
<template>
<div id="login-box">
<div id="login-box" v-loading="oauthLoading" element-loading-text="登录中...">
<div id="background-wrap" v-if="!settingStore.settings.background">
<div class="x1 cloud"></div>
<div class="x2 cloud"></div>
@@ -44,6 +44,9 @@
<el-button class="btn" type="primary" @click="submit" :loading="loginLoading"
>{{ $t('loginBtn') }}
</el-button>
<el-button class="btn" v-if="settingStore.settings.linuxdoSwitch" style="margin-top: 10px" @click="linuxDoLogin">
<el-avatar src="/image/linuxdo.webp" :size="18" style="margin-right: 10px" />LinuxDo
</el-button>
</div>
<div v-show="show !== 'login'">
<el-input class="email-input" v-model="registerForm.email" type="text" :placeholder="$t('emailAccount')"
@@ -88,9 +91,12 @@
>
<span style="font-size: 12px;color: #F56C6C" v-if="botJsError">{{ $t('verifyModuleFailed') }}</span>
</div>
<el-button class="btn" type="primary" @click="submitRegister" :loading="registerLoading"
<el-button class="btn" style="margin: 0" type="primary" @click="submitRegister" :loading="registerLoading"
>{{ $t('regBtn') }}
</el-button>
<el-button v-if="settingStore.settings.linuxdoSwitch" class="btn" style="margin-top: 10px" @click="linuxDoLogin">
<el-avatar src="/image/linuxdo.webp" :size="18" style="margin-right: 10px" />LinuxDo
</el-button>
</div>
<template v-if="settingStore.settings.register === 0">
<div class="switch" @click="show = 'register'" v-if="show === 'login'">{{ $t('noAccount') }}
@@ -100,6 +106,43 @@
</template>
</div>
</div>
<el-dialog class="bind-dialog" v-model="showBindForm" title="注册邮箱" >
<div class="bind-container">
<el-input v-model="bindForm.email" type="text" :placeholder="$t('emailAccount')" autocomplete="off">
<template #append>
<div @click.stop="openSelect">
<el-select
ref="mySelect"
v-model="suffix"
:placeholder="$t('select')"
class="select"
>
<el-option
v-for="item in domainList"
:key="item"
:label="item"
:value="item"
/>
</el-select>
<div>
<span>{{ suffix }}</span>
<Icon class="setting-icon" icon="mingcute:down-small-fill" width="20" height="20"/>
</div>
</div>
</template>
</el-input>
<el-input v-if="settingStore.settings.regKey === 0" v-model="bindForm.code" :placeholder="$t('regKey')"
type="text" autocomplete="off"/>
<el-input v-if="settingStore.settings.regKey === 2" v-model="bindForm.code"
:placeholder="$t('regKeyOptional')" type="text" autocomplete="off"/>
<el-button class="btn" type="primary" @click="bind" :loading="bindLoading"
>绑定
</el-button>
</div>
</el-dialog>
<a class="github" href="https://github.com/maillab/cloud-mail">
<Icon icon="mingcute:github-line" color="#1890ff" width="20" height="20" />
</a>
</div>
</template>
@@ -118,6 +161,7 @@ import {cvtR2Url} from "@/utils/convert.js";
import {loginUserInfo} from "@/request/my.js";
import {permsToRouter} from "@/perm/perm.js";
import {useI18n} from "vue-i18n";
import {oauthBindUser, oauthLinuxDoLogin} from "@/request/ouath.js";
const {t} = useI18n();
const accountStore = useAccountStore();
@@ -125,7 +169,17 @@ const userStore = useUserStore();
const uiStore = useUiStore();
const settingStore = useSettingStore();
const loginLoading = ref(false)
const bindLoading = ref(false)
const oauthLoading = ref(false);
const showBindForm = ref(false);
const show = ref('login')
const bindForm = reactive({
email: '',
oauthUserId: '',
code: ''
})
const form = reactive({
email: '',
password: '',
@@ -192,11 +246,100 @@ const background = computed(() => {
} : ''
})
const openSelect = () => {
mySelect.value.toggleMenu()
}
function linuxDoLogin() {
const clientId = settingStore.settings.linuxdoClientId
const redirectUri = encodeURIComponent(settingStore.settings.linuxdoCallbackUrl)
window.location.href =
`https://connect.linux.do/oauth2/authorize?client_id=${clientId}&redirect_uri=${redirectUri}&response_type=code&scope=openid+profile+email`
}
linuxDoGetUser();
async function linuxDoGetUser() {
const params = new URLSearchParams(window.location.search)
const code = params.get('code')
if (code) {
oauthLoading.value = true
oauthLinuxDoLogin(code).then(data => {
bindForm.oauthUserId = data.userInfo.oauthUserId;
if (!data.token) {
showBindForm.value = true
oauthLoading.value = false
ElMessage({
message: '请注册绑定一个邮箱',
type: 'warning',
duration: 4000,
plain: true,
})
return;
}
saveToken(data.token);
}).catch(() => {
oauthLoading.value = false
})
}
const cleanUrl = window.location.origin + window.location.pathname
window.history.replaceState({}, '', cleanUrl)
}
function bind() {
if (!bindForm.email) {
ElMessage({
message: t('emptyEmailMsg'),
type: 'error',
plain: true,
})
return
}
let email = bindForm.email + suffix.value;
if (!isEmail(email)) {
ElMessage({
message: t('notEmailMsg'),
type: 'error',
plain: true,
})
return
}
if (settingStore.settings.regKey === 0) {
if (!bindForm.code) {
ElMessage({
message: t('emptyRegKeyMsg'),
type: 'error',
plain: true,
})
return
}
}
const form = {email: bindForm.email + suffix.value, oauthUserId: bindForm.oauthUserId, code: bindForm.code}
bindLoading.value = true
oauthBindUser(form).then(data => {
saveToken(data.token)
}).catch(() => {
bindLoading.value = false
})
}
const submit = () => {
if (!form.email) {
@@ -230,21 +373,28 @@ const submit = () => {
loginLoading.value = true
login(email, form.password).then(async data => {
localStorage.setItem('token', data.token)
const user = await loginUserInfo();
accountStore.currentAccountId = user.accountId;
userStore.user = user;
const routers = permsToRouter(user.permKeys);
routers.forEach(routerData => {
router.addRoute('layout', routerData);
});
await router.replace({name: 'layout'})
uiStore.showNotice()
await saveToken(data.token)
}).finally(() => {
loginLoading.value = false
})
}
async function saveToken(token) {
console.log(token)
localStorage.setItem('token', token)
const user = await loginUserInfo();
accountStore.currentAccountId = user.accountId;
userStore.user = user;
const routers = permsToRouter(user.permKeys);
routers.forEach(routerData => {
router.addRoute('layout', routerData);
});
await router.replace({name: 'layout'})
uiStore.showNotice()
oauthLoading.value = false;
bindLoading.value = false;
}
function submitRegister() {
@@ -485,11 +635,43 @@ function submitRegister() {
padding: 0 10px;
}
:deep(.bind-dialog) {
width: 400px !important;
@media (max-width: 440px) {
width: calc(100% - 40px) !important;
margin-right: 20px !important;
margin-left: 20px !important;
}
}
.bind-container {
display: grid;
grid-template-columns: 1fr;
gap: 15px;
}
.setting-icon {
position: relative;
top: 6px;
}
.github {
position: fixed;
width: 35px;
height: 35px;
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
background: var(--el-bg-color);
bottom: 10px;
right: 10px;
z-index: 1000;
border: 1px solid var(--el-border-color-light);
box-shadow: var(--el-box-shadow-light);
cursor: pointer;
}
:deep(.el-input-group__append) {
padding: 0 !important;
padding-left: 8px !important;
@@ -498,6 +680,10 @@ function submitRegister() {
border-radius: 0 8px 8px 0;
}
:deep(.el-button+.el-button) {
margin: 0;
}
.register-turnstile {
margin-bottom: 18px;
}
+2 -2
View File
@@ -349,7 +349,7 @@
<div class="concerning-item">
<span>{{ $t('version') }} :</span>
<el-badge is-dot :hidden="!hasUpdate">
<el-button @click="jump('https://github.com/eoao/cloud-mail/releases')">
<el-button @click="jump('https://github.com/eoao/maillab/releases')">
{{ currentVersion }}
<template #icon>
<Icon icon="qlementine-icons:version-control-16" style="font-size: 20px" color="#1890FF"/>
@@ -360,7 +360,7 @@
<div class="concerning-item">
<span>{{ $t('community') }} : </span>
<div class="community">
<el-button @click="jump('https://github.com/eoao/cloud-mail')">
<el-button @click="jump('https://github.com/maillab/cloud-mail')">
Github
<template #icon>
<Icon icon="codicon:github-inverted" width="22" height="22"/>
+16 -1
View File
@@ -43,6 +43,13 @@
<el-table-column :width="expandWidth" type="expand">
<template #default="props">
<div class="details">
<div v-if="props.row.username"><span class="details-item-title">LinuxDo:</span>
<el-avatar :src="props.row.avatar" :size="30" class="linuxdo-avatar" />
<span style="margin: 0 10px">用户名{{props.row.username}}</span>
<span>
等级<el-tag type="success">{{props.row.trustLevel}}</el-tag>
</span>
</div>
<div v-if="!sendNumShow"><span
class="details-item-title">{{ $t('tabSent') }}:</span>{{ props.row.sendEmailCount }}
</div>
@@ -102,7 +109,10 @@
<el-table-column show-overflow-tooltip :tooltip-formatter="tableRowFormatter" :label="$t('tabEmailAddress')"
:min-width="emailWidth">
<template #default="props">
<div class="email-row">{{ props.row.email }}</div>
<div style="display: flex;gap: 5px">
<div class="email-row">{{ props.row.email }}</div>
<el-tag type="warning" v-if="props.row.username">L</el-tag>
</div>
</template>
</el-table-column>
<el-table-column :formatter="formatterReceive" label-class-name="receive" column-key="receive"
@@ -995,6 +1005,11 @@ function adjustWidth() {
}
}
:deep(.linuxdo-avatar) {
position: relative !important;
top: 10px;
}
.account-pagination {
display: flex;
justify-content: end;