新增 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
+13
View File
@@ -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))
})
+16
View File
@@ -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()
});
+1
View File
@@ -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;
+2
View File
@@ -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 })
},
};
+19 -9
View File
@@ -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;`,
+2 -1
View File
@@ -17,7 +17,8 @@ const exclude = [
'/init',
'/public/genToken',
'/telegram',
'/test'
'/test',
'/oauth'
];
const requirePerms = [
+9 -6
View File
@@ -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'));
}
+115
View File
@@ -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
+20 -1
View File
@@ -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
};
}
};
+10 -1
View File
@@ -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));
+5
View File
@@ -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"