mirror of
https://github.com/schroinerxy/cloud-mail.git
synced 2026-06-21 19:35:50 +08:00
新增 LinuxDo Oauth2 登录
This commit is contained in:
@@ -22,6 +22,10 @@ jobs:
|
||||
DOMAIN: ${{ secrets.DOMAIN }}
|
||||
ADMIN: ${{ secrets.ADMIN }}
|
||||
JWT_SECRET: ${{ secrets.JWT_SECRET }}
|
||||
LINUXDO_CLIENT_ID: ${{ secrets.LINUXDO_CLIENT_ID }}
|
||||
LINUXDO_CLIENT_SECRET: ${{ secrets.LINUXDO_CLIENT_SECRET }}
|
||||
LINUXDO_CALLBACK_URL: ${{ secrets.LINUXDO_CALLBACK_URL }}
|
||||
LINUXDO_SWITCH: ${{ secrets.LINUXDO_SWITCH }}
|
||||
|
||||
outputs:
|
||||
deployment_skipped: ${{ steps.deploy.outputs.deployment_skipped }}
|
||||
@@ -78,6 +82,10 @@ jobs:
|
||||
sed -i "s|\"\${DOMAIN}\"|${DOMAIN}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${ADMIN}|${ADMIN}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${JWT_SECRET}|${JWT_SECRET}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_CLIENT_ID}|${LINUXDO_CLIENT_ID}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_CLIENT_SECRET}|${LINUXDO_CLIENT_SECRET}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_CALLBACK_URL}|${LINUXDO_CALLBACK_URL}|g" "$CONFIG_FILE"
|
||||
sed -i "s|\${LINUXDO_SWITCH}|${LINUXDO_SWITCH}|g" "$CONFIG_FILE"
|
||||
|
||||
echo "🔍 Debug: Checking configuration after replacement..."
|
||||
echo "R2_BUCKET_NAME value: '$R2_BUCKET_NAME'"
|
||||
|
||||
+3
-2
@@ -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,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 |
@@ -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)
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"/>
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
import app from '../hono/hono';
|
||||
import result from "../model/result";
|
||||
import oauthService from "../service/oauth-service";
|
||||
|
||||
app.post('/oauth/linuxDo/login', async (c) => {
|
||||
const loginInfo = await oauthService.linuxDoLogin(c, await c.req.json());
|
||||
return c.json(result.ok(loginInfo))
|
||||
});
|
||||
|
||||
app.put('/oauth/bindUser', async (c) => {
|
||||
const loginInfo = await oauthService.bindUser(c, await c.req.json());
|
||||
return c.json(result.ok(loginInfo))
|
||||
})
|
||||
@@ -0,0 +1,16 @@
|
||||
import { sqliteTable, integer, text } from 'drizzle-orm/sqlite-core';
|
||||
import { sql } from 'drizzle-orm';
|
||||
|
||||
export const oauth = sqliteTable('oauth', {
|
||||
oauthId: integer('oauth_id').primaryKey({ autoIncrement: true }),
|
||||
oauthUserId: text('oauth_user_id'),
|
||||
username: text('username'),
|
||||
name: text('name'),
|
||||
avatar: text('avatar'),
|
||||
active: integer('active'),
|
||||
trustLevel: integer('trust_level'),
|
||||
silenced: integer('silenced'),
|
||||
createTime: text('create_time').default(sql`CURRENT_TIMESTAMP`).notNull(),
|
||||
userId: integer('user_id').default(0).notNull()
|
||||
});
|
||||
|
||||
@@ -19,4 +19,5 @@ import '../api/analysis-api'
|
||||
import '../api/reg-key-api'
|
||||
import '../api/public-api'
|
||||
import '../api/telegram-api'
|
||||
import '../api/oauth-api'
|
||||
export default app;
|
||||
|
||||
@@ -4,6 +4,7 @@ import userService from './service/user-service';
|
||||
import verifyRecordService from './service/verify-record-service';
|
||||
import emailService from './service/email-service';
|
||||
import kvObjService from './service/kv-obj-service';
|
||||
import oauthService from "./service/oauth-service";
|
||||
export default {
|
||||
async fetch(req, env, ctx) {
|
||||
|
||||
@@ -26,5 +27,6 @@ export default {
|
||||
await verifyRecordService.clearRecord({ env })
|
||||
await userService.resetDaySendCount({ env })
|
||||
await emailService.completeReceiveAll({ env })
|
||||
await oauthService.clearNoBindOathUser({ env })
|
||||
},
|
||||
};
|
||||
|
||||
@@ -23,10 +23,28 @@ const init = {
|
||||
await this.v1_7DB(c);
|
||||
await this.v2DB(c);
|
||||
await this.v2_3DB(c);
|
||||
await this.v2_4DB(c);
|
||||
await settingService.refresh(c);
|
||||
return c.text(t('initSuccess'));
|
||||
},
|
||||
|
||||
async v2_4DB(c) {
|
||||
await c.env.db.prepare(`
|
||||
CREATE TABLE IF NOT EXISTS oauth (
|
||||
oauth_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
oauth_user_id TEXT,
|
||||
username TEXT,
|
||||
name TEXT,
|
||||
avatar TEXT,
|
||||
active INTEGER,
|
||||
trust_level INTEGER,
|
||||
silenced INTEGER,
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
user_id INTEGER NOT NULL DEFAULT 0
|
||||
)
|
||||
`).run();
|
||||
},
|
||||
|
||||
async v2_3DB(c) {
|
||||
try {
|
||||
await c.env.db.batch([
|
||||
@@ -75,15 +93,7 @@ const init = {
|
||||
|
||||
const noticeContent = '本项目仅供学习交流,禁止用于违法业务\n' +
|
||||
'<br>\n' +
|
||||
'请遵守当地法规,作者不承担任何法律责任\n' +
|
||||
'<div style="display: flex;gap: 18px;margin-top: 10px;">\n' +
|
||||
'<a href="https://github.com/eoao/cloud-mail" target="_blank" >\n' +
|
||||
'<img src="https://api.iconify.design/codicon:github-inverted.svg" alt="GitHub" width="25" height="25" />\n' +
|
||||
'</a>\n' +
|
||||
'<a href="https://t.me/cloud_mail_tg" target="_blank" >\n' +
|
||||
'<img src="https://api.iconify.design/logos:telegram.svg" alt="GitHub" width="25" height="25" />\n' +
|
||||
'</a>\n' +
|
||||
'</div>\n'
|
||||
'请遵守当地法规,作者不承担任何法律责任'
|
||||
|
||||
const ADD_COLUMN_SQL_LIST = [
|
||||
`ALTER TABLE setting ADD COLUMN reg_verify_count INTEGER NOT NULL DEFAULT 1;`,
|
||||
|
||||
@@ -17,7 +17,8 @@ const exclude = [
|
||||
'/init',
|
||||
'/public/genToken',
|
||||
'/telegram',
|
||||
'/test'
|
||||
'/test',
|
||||
'/oauth'
|
||||
];
|
||||
|
||||
const requirePerms = [
|
||||
|
||||
@@ -22,12 +22,16 @@ import verifyRecordService from './verify-record-service';
|
||||
|
||||
const loginService = {
|
||||
|
||||
async register(c, params) {
|
||||
async register(c, params, oauth = false) {
|
||||
|
||||
const { email, password, token, code } = params;
|
||||
|
||||
const {regKey, register, registerVerify, regVerifyCount} = await settingService.query(c)
|
||||
let {regKey, register, registerVerify, regVerifyCount} = await settingService.query(c)
|
||||
|
||||
if (oauth) {
|
||||
registerVerify = settingConst.registerVerify.CLOSE;
|
||||
register = settingConst.register.OPEN;
|
||||
}
|
||||
|
||||
if (register === settingConst.register.CLOSE) {
|
||||
throw new BizError(t('regDisabled'));
|
||||
@@ -78,7 +82,6 @@ const loginService = {
|
||||
throw new BizError(t('isRegAccount'));
|
||||
}
|
||||
|
||||
|
||||
let defType = null
|
||||
|
||||
if (!type) {
|
||||
@@ -188,11 +191,11 @@ const loginService = {
|
||||
return { type: regKeyRow.roleId, regKeyId: regKeyRow.regKeyId };
|
||||
},
|
||||
|
||||
async login(c, params) {
|
||||
async login(c, params, noVerifyPwd = false) {
|
||||
|
||||
const { email, password } = params;
|
||||
|
||||
if (!email || !password) {
|
||||
if ((!email || !password) && !noVerifyPwd) {
|
||||
throw new BizError(t('emailAndPwdEmpty'));
|
||||
}
|
||||
|
||||
@@ -210,7 +213,7 @@ const loginService = {
|
||||
throw new BizError(t('isBanUser'));
|
||||
}
|
||||
|
||||
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password)) {
|
||||
if (!await cryptoUtils.verifyPassword(password, userRow.salt, userRow.password) && !noVerifyPwd) {
|
||||
throw new BizError(t('IncorrectPwd'));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
import BizError from "../error/biz-error";
|
||||
import orm from "../entity/orm";
|
||||
import {oauth} from "../entity/oauth";
|
||||
import { eq } from 'drizzle-orm';
|
||||
import userService from "./user-service";
|
||||
import loginService from "./login-service";
|
||||
import cryptoUtils from "../utils/crypto-utils";
|
||||
|
||||
const oauthService = {
|
||||
|
||||
async bindUser(c, params) {
|
||||
|
||||
const { email, oauthUserId, code } = params;
|
||||
|
||||
const oauthRow = await this.getById(c, oauthUserId);
|
||||
|
||||
let userRow = await userService.selectByIdIncludeDel(c, oauthRow.userId);
|
||||
|
||||
if (userRow) {
|
||||
throw new BizError('用户已绑定有邮箱')
|
||||
}
|
||||
|
||||
await loginService.register(c, { email, password: cryptoUtils.genRandomPwd(), code }, true);
|
||||
|
||||
userRow = await userService.selectByEmail(c, email);
|
||||
|
||||
orm(c).update(oauth).set({ userId: userRow.userId }).where(eq(oauth.oauthUserId, oauthUserId)).run();
|
||||
const jwtToken = await loginService.login(c, { email, password: null }, true);
|
||||
|
||||
return { userInfo: oauthRow, token: jwtToken}
|
||||
},
|
||||
|
||||
async linuxDoLogin(c, params) {
|
||||
|
||||
const { code } = params;
|
||||
|
||||
let token = '';
|
||||
let userInfo = {}
|
||||
|
||||
const reqParams = new URLSearchParams()
|
||||
reqParams.append('client_id', c.env.linuxdo_client_id)
|
||||
reqParams.append('client_secret', c.env.linuxdo_client_secret)
|
||||
reqParams.append('code', code)
|
||||
reqParams.append('redirect_uri', c.env.linuxdo_callback_url)
|
||||
reqParams.append('grant_type', 'authorization_code')
|
||||
|
||||
const tokenRes = await fetch("https://connect.linux.do/oauth2/token", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
||||
body: reqParams.toString()
|
||||
})
|
||||
|
||||
if (!tokenRes.ok) {
|
||||
throw new BizError(tokenRes.statusText)
|
||||
}
|
||||
|
||||
token = await tokenRes.json()
|
||||
|
||||
const userRes = await fetch('https://connect.linux.do/api/user', {
|
||||
headers: {
|
||||
Authorization: 'Bearer ' + token.access_token
|
||||
}
|
||||
});
|
||||
|
||||
if (!userRes.ok) {
|
||||
throw new BizError(userRes.statusText)
|
||||
}
|
||||
|
||||
userInfo = await userRes.json();
|
||||
|
||||
userInfo.oauthUserId = String(userInfo.id);
|
||||
userInfo.active = userInfo.active ? 0 : 1;
|
||||
userInfo.silenced = userInfo.active ? 0 : 1;
|
||||
userInfo.trustLevel = userInfo.trust_level;
|
||||
userInfo.avatar = userInfo.avatar_url;
|
||||
|
||||
const oauthRow = await this.saveUser(c, userInfo);
|
||||
const userRow = await userService.selectByIdIncludeDel(c, oauthRow.userId);
|
||||
|
||||
if (!userRow) {
|
||||
return { userInfo: oauthRow, token: null }
|
||||
}
|
||||
|
||||
const JwtToken = await loginService.login(c, { email: userRow.email, password: null }, true);
|
||||
return { userInfo: oauthRow, token: JwtToken }
|
||||
},
|
||||
|
||||
async saveUser(c, userInfo) {
|
||||
|
||||
const userInfoRow = await this.getById(c, userInfo.oauthUserId);
|
||||
|
||||
if (!userInfoRow) {
|
||||
return await orm(c).insert(oauth).values(userInfo).returning().get();
|
||||
} else {
|
||||
return await orm(c).update(oauth).set(userInfo).where(eq(oauth.oauthUserId, userInfo.oauthUserId)).returning().get();
|
||||
}
|
||||
|
||||
},
|
||||
|
||||
async getById(c, oauthUserId) {
|
||||
return await orm(c).select().from(oauth).where(eq(oauth.oauthUserId, oauthUserId)).get();
|
||||
},
|
||||
|
||||
async deleteByUserId(c, userId) {
|
||||
await orm(c).delete(oauth).where(eq(oauth.userId, userId)).run();
|
||||
},
|
||||
|
||||
//定时任务凌晨清除未绑定邮箱的oauth用户
|
||||
async clearNoBindOathUser(c) {
|
||||
await orm(c).delete(oauth).where(eq(oauth.userId, 0)).run();
|
||||
},
|
||||
|
||||
}
|
||||
|
||||
export default oauthService
|
||||
@@ -42,6 +42,22 @@ const settingService = {
|
||||
|
||||
domainList = domainList.map(item => '@' + item);
|
||||
setting.domainList = domainList;
|
||||
|
||||
|
||||
let linuxdoSwitch = c.env.linuxdo_switch;
|
||||
|
||||
if (typeof linuxdoSwitch === 'string' && linuxdoSwitch === 'true') {
|
||||
linuxdoSwitch = true
|
||||
} else if (linuxdoSwitch === true) {
|
||||
linuxdoSwitch = true
|
||||
} else {
|
||||
linuxdoSwitch = false
|
||||
}
|
||||
|
||||
setting.linuxdoClientId = c.env.linuxdo_client_id;
|
||||
setting.linuxdoCallbackUrl = c.env.linuxdo_callback_url;
|
||||
setting.linuxdoSwitch = linuxdoSwitch;
|
||||
|
||||
c.set?.('setting', setting);
|
||||
return setting;
|
||||
},
|
||||
@@ -187,7 +203,10 @@ const settingService = {
|
||||
noticeWidth: settingRow.noticeWidth,
|
||||
noticeOffset: settingRow.noticeOffset,
|
||||
notice: settingRow.notice,
|
||||
loginDomain: settingRow.loginDomain
|
||||
loginDomain: settingRow.loginDomain,
|
||||
linuxdoClientId: settingRow.linuxdoClientId,
|
||||
linuxdoCallbackUrl: settingRow.linuxdoCallbackUrl,
|
||||
linuxdoSwitch: settingRow.linuxdoSwitch
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -16,6 +16,8 @@ import saltHashUtils from '../utils/crypto-utils';
|
||||
import constant from '../const/constant';
|
||||
import { t } from '../i18n/i18n'
|
||||
import reqUtils from '../utils/req-utils';
|
||||
import {oauth} from "../entity/oauth";
|
||||
import oauthService from "./oauth-service";
|
||||
|
||||
const userService = {
|
||||
|
||||
@@ -94,6 +96,7 @@ const userService = {
|
||||
async physicsDelete(c, params) {
|
||||
const { userId } = params
|
||||
await accountService.physicsDeleteByUserIds(c, [userId])
|
||||
await oauthService.deleteByUserId(c, userId);
|
||||
await orm(c).delete(user).where(eq(user.userId, userId)).run();
|
||||
await c.env.kv.delete(kvConst.AUTH_INFO + userId);
|
||||
},
|
||||
@@ -130,7 +133,13 @@ const userService = {
|
||||
}
|
||||
|
||||
|
||||
const query = orm(c).select().from(user)
|
||||
const query = orm(c).select({
|
||||
...user,
|
||||
username: oauth.username,
|
||||
trustLevel: oauth.trustLevel,
|
||||
avatar: oauth.avatar,
|
||||
name: oauth.name
|
||||
}).from(user).leftJoin(oauth, eq(oauth.userId, user.userId))
|
||||
.where(and(...conditions));
|
||||
|
||||
|
||||
|
||||
@@ -35,5 +35,10 @@ domain = "${DOMAIN}" #邮件域名可可配置多个 示例: ["example1.com",
|
||||
admin = "${ADMIN}" #管理员的邮箱 示例: admin@example.com
|
||||
jwt_secret = "${JWT_SECRET}" #jwt令牌的密钥,随便填一串字符串
|
||||
|
||||
linuxdo_client_id = "${LINUXDO_CLIENT_ID}"
|
||||
linuxdo_client_secret = "${LINUXDO_CLIENT_SECRET}"
|
||||
linuxdo_callback_url = "${LINUXDO_CALLBACK_URL}"
|
||||
linuxdo_switch = "${LINUXDO_SWITCH}"
|
||||
|
||||
[build]
|
||||
command = "pnpm --prefix ../mail-vue install && pnpm --prefix ../mail-vue run build"
|
||||
|
||||
Reference in New Issue
Block a user