修改认证信息存储方式,避免认证信息异常

This commit is contained in:
whyour 2024-12-30 14:23:04 +08:00
parent 75f91e1473
commit 678e3e2dc6
14 changed files with 326 additions and 230 deletions

View File

@ -2,22 +2,22 @@
**/*.svg **/*.svg
**/*.ejs **/*.ejs
**/*.html **/*.html
.umi /.umi
.umi-production /.umi-production
.umi-test /.umi-test
.history /.history
.tmp /.tmp
node_modules /node_modules
npm-debug.log* npm-debug.log*
yarn-error.log yarn-error.log
yarn.lock yarn.lock
package-lock.json package-lock.json
static /static
data /data
DS_Store DS_Store
src/.umi /src/.umi
src/.umi-production /src/.umi-production
src/.umi-test /src/.umi-test
.env.local .env.local
.env .env
version.ts version.ts

View File

@ -33,7 +33,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getAuthInfo();
const { version, changeLog, changeLogLink, publishTime } = const { version, changeLog, changeLogLink, publishTime } =
await parseVersion(config.versionFile); await parseVersion(config.versionFile);

View File

@ -90,7 +90,7 @@ export default (app: Router) => {
const logger: Logger = Container.get('logger'); const logger: Logger = Container.get('logger');
try { try {
const userService = Container.get(UserService); const userService = Container.get(UserService);
const authInfo = await userService.getUserInfo(); const authInfo = await userService.getAuthInfo();
res.send({ res.send({
code: 200, code: 200,
data: { data: {

View File

@ -27,6 +27,7 @@ export enum AuthDataType {
'notification' = 'notification', 'notification' = 'notification',
'removeLogFrequency' = 'removeLogFrequency', 'removeLogFrequency' = 'removeLogFrequency',
'systemConfig' = 'systemConfig', 'systemConfig' = 'systemConfig',
'authConfig' = 'authConfig',
} }
export interface SystemConfigInfo { export interface SystemConfigInfo {
@ -46,11 +47,31 @@ export interface LoginLogInfo {
status?: LoginStatus; status?: LoginStatus;
} }
export interface AuthInfo {
username: string;
password: string;
retries: number;
lastlogon: number;
lastip: string;
lastaddr: string;
platform: string;
isTwoFactorChecking: boolean;
token: string;
tokens: Record<string, string>;
twoFactorActivated: boolean;
twoFactorActived: boolean;
twoFactorSecret: string;
avatar: string;
}
export type SystemModelInfo = SystemConfigInfo & export type SystemModelInfo = SystemConfigInfo &
Partial<NotificationInfo> & Partial<NotificationInfo> &
LoginLogInfo; LoginLogInfo &
Partial<AuthInfo>;
export interface SystemInstance extends Model<SystemInfo, SystemInfo>, SystemInfo { } export interface SystemInstance
extends Model<SystemInfo, SystemInfo>,
SystemInfo {}
export const SystemModel = sequelize.define<SystemInstance>('Auth', { export const SystemModel = sequelize.define<SystemInstance>('Auth', {
ip: DataTypes.STRING, ip: DataTypes.STRING,
type: DataTypes.STRING, type: DataTypes.STRING,

View File

@ -4,18 +4,14 @@ import cors from 'cors';
import routes from '../api'; import routes from '../api';
import config from '../config'; import config from '../config';
import { UnauthorizedError, expressjwt } from 'express-jwt'; import { UnauthorizedError, expressjwt } from 'express-jwt';
import fs from 'fs/promises'; import { getPlatform, getToken } from '../config/util';
import { getPlatform, getToken, safeJSONParse } from '../config/util';
import Container from 'typedi';
import OpenService from '../services/open';
import rewrite from 'express-urlrewrite'; import rewrite from 'express-urlrewrite';
import UserService from '../services/user';
import * as Sentry from '@sentry/node'; import * as Sentry from '@sentry/node';
import { EnvModel } from '../data/env';
import { errors } from 'celebrate'; import { errors } from 'celebrate';
import { createProxyMiddleware } from 'http-proxy-middleware'; import { createProxyMiddleware } from 'http-proxy-middleware';
import { serveEnv } from '../config/serverEnv'; import { serveEnv } from '../config/serverEnv';
import Logger from './logger'; import Logger from './logger';
import { IKeyvStore, shareStore } from '../shared/store';
export default ({ app }: { app: Application }) => { export default ({ app }: { app: Application }) => {
app.set('trust proxy', 'loopback'); app.set('trust proxy', 'loopback');
@ -58,8 +54,10 @@ export default ({ app }: { app: Application }) => {
app.use(async (req, res, next) => { app.use(async (req, res, next) => {
const headerToken = getToken(req); const headerToken = getToken(req);
if (req.path.startsWith('/open/')) { if (req.path.startsWith('/open/')) {
const openService = Container.get(OpenService); const apps = await shareStore.getApps();
const doc = await openService.findTokenByValue(headerToken); const doc = apps?.filter((x) =>
x.tokens?.find((y) => y.value === headerToken),
)?.[0];
if (doc && doc.tokens && doc.tokens.length > 0) { if (doc && doc.tokens && doc.tokens.length > 0) {
const currentToken = doc.tokens.find((x) => x.value === headerToken); const currentToken = doc.tokens.find((x) => x.value === headerToken);
const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/); const keyMatch = req.path.match(/\/open\/([a-z]+)\/*/);
@ -83,9 +81,9 @@ export default ({ app }: { app: Application }) => {
return next(); return next();
} }
const data = await fs.readFile(config.authConfigFile, 'utf8'); const authInfo = await shareStore.getAuthInfo();
if (data && headerToken) { if (authInfo && headerToken) {
const { token = '', tokens = {} } = safeJSONParse(data); const { token = '', tokens = {} } = authInfo;
if (headerToken === token || tokens[req.platform] === headerToken) { if (headerToken === token || tokens[req.platform] === headerToken) {
return next(); return next();
} }
@ -103,8 +101,8 @@ export default ({ app }: { app: Application }) => {
if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) { if (!['/api/user/init', '/api/user/notification/init'].includes(req.path)) {
return next(); return next();
} }
const userService = Container.get(UserService); const authInfo =
const authInfo = await userService.getUserInfo(); (await shareStore.getAuthInfo()) || ({} as IKeyvStore['authInfo']);
let isInitialized = true; let isInitialized = true;
if ( if (

View File

@ -12,7 +12,10 @@ import { initPosition } from '../data/env';
import { AuthDataType, SystemModel } from '../data/system'; import { AuthDataType, SystemModel } from '../data/system';
import SystemService from '../services/system'; import SystemService from '../services/system';
import UserService from '../services/user'; import UserService from '../services/user';
import { writeFile } from 'fs/promises'; import { writeFile, readFile } from 'fs/promises';
import { safeJSONParse } from '../config/util';
import OpenService from '../services/open';
import { shareStore } from '../shared/store';
export default async () => { export default async () => {
const cronService = Container.get(CronService); const cronService = Container.get(CronService);
@ -20,10 +23,38 @@ export default async () => {
const dependenceService = Container.get(DependenceService); const dependenceService = Container.get(DependenceService);
const systemService = Container.get(SystemService); const systemService = Container.get(SystemService);
const userService = Container.get(UserService); const userService = Container.get(UserService);
const openService = Container.get(OpenService);
// 初始化增加系统配置 // 初始化增加系统配置
await SystemModel.upsert({ type: AuthDataType.systemConfig }); await SystemModel.upsert({ type: AuthDataType.systemConfig });
await SystemModel.upsert({ type: AuthDataType.notification }); await SystemModel.upsert({ type: AuthDataType.notification });
await SystemModel.upsert({ type: AuthDataType.authConfig });
const authConfig = await SystemModel.findOne({
where: { type: AuthDataType.authConfig },
});
if (!authConfig?.info) {
let authInfo = {
username: 'admin',
password: 'admin',
};
try {
const content = await readFile(config.authConfigFile, 'utf8');
authInfo = safeJSONParse(content);
} catch (error) {}
if (authConfig?.id) {
await SystemModel.update(
{ info: authInfo },
{
where: { id: authConfig.id },
},
);
} else {
await SystemModel.create({
info: authInfo,
type: AuthDataType.authConfig,
});
}
}
// 初始化通知配置 // 初始化通知配置
const notifyConfig = await userService.getNotificationMode(); const notifyConfig = await userService.getNotificationMode();
@ -169,4 +200,11 @@ export default async () => {
// 初始化保存一次ck和定时任务数据 // 初始化保存一次ck和定时任务数据
await cronService.autosave_crontab(); await cronService.autosave_crontab();
await envService.set_envs(); await envService.set_envs();
const authInfo = await userService.getAuthInfo();
const apps = await openService.findApps();
await shareStore.updateAuthInfo(authInfo);
if (apps?.length) {
await shareStore.updateApps(apps);
}
}; };

View File

@ -20,9 +20,7 @@ const bakPath = path.join(dataPath, 'bak/');
const samplePath = path.join(rootPath, 'sample/'); const samplePath = path.join(rootPath, 'sample/');
const tmpPath = path.join(logPath, '.tmp/'); const tmpPath = path.join(logPath, '.tmp/');
const confFile = path.join(configPath, 'config.sh'); const confFile = path.join(configPath, 'config.sh');
const authConfigFile = path.join(configPath, 'auth.json');
const sampleConfigFile = path.join(samplePath, 'config.sample.sh'); const sampleConfigFile = path.join(samplePath, 'config.sample.sh');
const sampleAuthFile = path.join(samplePath, 'auth.sample.json');
const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh'); const sampleTaskShellFile = path.join(samplePath, 'task.sample.sh');
const sampleNotifyJsFile = path.join(samplePath, 'notify.js'); const sampleNotifyJsFile = path.join(samplePath, 'notify.js');
const sampleNotifyPyFile = path.join(samplePath, 'notify.py'); const sampleNotifyPyFile = path.join(samplePath, 'notify.py');
@ -40,7 +38,6 @@ const sshdPath = path.join(dataPath, 'ssh.d');
const systemLogPath = path.join(dataPath, 'syslog'); const systemLogPath = path.join(dataPath, 'syslog');
export default async () => { export default async () => {
const authFileExist = await fileExist(authConfigFile);
const confFileExist = await fileExist(confFile); const confFileExist = await fileExist(confFile);
const scriptDirExist = await fileExist(scriptPath); const scriptDirExist = await fileExist(scriptPath);
const preloadDirExist = await fileExist(preloadPath); const preloadDirExist = await fileExist(preloadPath);
@ -100,9 +97,6 @@ export default async () => {
} }
// 初始化文件 // 初始化文件
if (!authFileExist) {
await fs.writeFile(authConfigFile, await fs.readFile(sampleAuthFile));
}
if (!confFileExist) { if (!confFileExist) {
await fs.writeFile(confFile, await fs.readFile(sampleConfigFile)); await fs.writeFile(confFile, await fs.readFile(sampleConfigFile));

View File

@ -2,9 +2,8 @@ import sockJs from 'sockjs';
import { Server } from 'http'; import { Server } from 'http';
import { Container } from 'typedi'; import { Container } from 'typedi';
import SockService from '../services/sock'; import SockService from '../services/sock';
import config from '../config/index'; import { getPlatform } from '../config/util';
import fs from 'fs/promises'; import { shareStore } from '../shared/store';
import { getPlatform, safeJSONParse } from '../config/util';
export default async ({ server }: { server: Server }) => { export default async ({ server }: { server: Server }) => {
const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} }); const echo = sockJs.createServer({ prefix: '/api/ws', log: () => {} });
@ -15,11 +14,11 @@ export default async ({ server }: { server: Server }) => {
conn.close('404'); conn.close('404');
} }
const data = await fs.readFile(config.authConfigFile, 'utf8'); const authInfo = await shareStore.getAuthInfo();
const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop'; const platform = getPlatform(conn.headers['user-agent'] || '') || 'desktop';
const headerToken = conn.url.replace(`${conn.pathname}?token=`, ''); const headerToken = conn.url.replace(`${conn.pathname}?token=`, '');
if (data) { if (authInfo) {
const { token = '', tokens = {} } = safeJSONParse(data); const { token = '', tokens = {} } = authInfo;
if (headerToken === token || tokens[platform] === headerToken) { if (headerToken === token || tokens[platform] === headerToken) {
sockService.addClient(conn); sockService.addClient(conn);

View File

@ -1,19 +1,18 @@
import { Service, Inject } from 'typedi'; import { Service, Inject } from 'typedi';
import winston from 'winston'; import winston from 'winston';
import { createRandomString } from '../config/util'; import { createRandomString } from '../config/util';
import config from '../config';
import { App, AppModel } from '../data/open'; import { App, AppModel } from '../data/open';
import { v4 as uuidV4 } from 'uuid'; import { v4 as uuidV4 } from 'uuid';
import sequelize, { Op } from 'sequelize'; import sequelize, { Op } from 'sequelize';
import { shareStore } from '../shared/store';
@Service() @Service()
export default class OpenService { export default class OpenService {
constructor(@Inject('logger') private logger: winston.Logger) {} constructor(@Inject('logger') private logger: winston.Logger) {}
public async findTokenByValue(token: string): Promise<App | null> { public async findApps(): Promise<App[] | null> {
const docs = await this.find({}); const docs = await this.find({});
const doc = docs.filter((x) => x.tokens?.find((y) => y.value === token)); return docs;
return doc[0];
} }
public async create(payload: App): Promise<App> { public async create(payload: App): Promise<App> {
@ -34,17 +33,19 @@ export default class OpenService {
name: payload.name, name: payload.name,
scopes: payload.scopes, scopes: payload.scopes,
id: payload.id, id: payload.id,
} as any); } as App);
return { ...newDoc, tokens: [] }; return { ...newDoc, tokens: [] };
} }
private async updateDb(payload: App): Promise<App> { private async updateDb(payload: Partial<App>): Promise<App> {
await AppModel.update(payload, { where: { id: payload.id } }); await AppModel.update(payload, { where: { id: payload.id } });
return await this.getDb({ id: payload.id }); const apps = await this.find({});
await shareStore.updateApps(apps);
return apps?.find((x) => x.id === payload.id) as App;
} }
public async getDb(query: any): Promise<App> { public async getDb(query: Record<string, any>): Promise<App> {
const doc: any = await AppModel.findOne({ where: query }); const doc = await AppModel.findOne({ where: query });
if (!doc) { if (!doc) {
throw new Error(`App ${JSON.stringify(query)} not found`); throw new Error(`App ${JSON.stringify(query)} not found`);
} }
@ -56,7 +57,7 @@ export default class OpenService {
} }
public async resetSecret(id: number): Promise<App> { public async resetSecret(id: number): Promise<App> {
const tab: any = { const tab: Partial<App> = {
client_secret: createRandomString(24, 24), client_secret: createRandomString(24, 24),
tokens: [], tokens: [],
id, id,
@ -74,7 +75,7 @@ export default class OpenService {
public async list( public async list(
searchText: string = '', searchText: string = '',
sort: any = {}, sort: any = {},
query: any = {}, query: Record<string, any> = {},
): Promise<App[]> { ): Promise<App[]> {
let condition = { ...query }; let condition = { ...query };
if (searchText) { if (searchText) {
@ -101,7 +102,7 @@ export default class OpenService {
} }
} }
private async find(query: any, sort?: any): Promise<App[]> { private async find(query: Record<string, any>, sort?: any): Promise<App[]> {
const docs = await AppModel.findAll({ where: { ...query } }); const docs = await AppModel.findAll({ where: { ...query } });
return docs.map((x) => x.get({ plain: true })); return docs.map((x) => x.get({ plain: true }));
} }

View File

@ -18,6 +18,7 @@ import {
SystemModel, SystemModel,
SystemModelInfo, SystemModelInfo,
LoginStatus, LoginStatus,
AuthInfo,
} from '../data/system'; } from '../data/system';
import { NotificationInfo } from '../data/notify'; import { NotificationInfo } from '../data/notify';
import NotificationService from './notify'; import NotificationService from './notify';
@ -28,6 +29,7 @@ import dayjs from 'dayjs';
import IP2Region from 'ip2region'; import IP2Region from 'ip2region';
import requestIp from 'request-ip'; import requestIp from 'request-ip';
import uniq from 'lodash/uniq'; import uniq from 'lodash/uniq';
import { shareStore } from '../shared/store';
@Service() @Service()
export default class UserService { export default class UserService {
@ -48,161 +50,142 @@ export default class UserService {
req: Request, req: Request,
needTwoFactor = true, needTwoFactor = true,
): Promise<any> { ): Promise<any> {
const _exist = await fileExist(config.authConfigFile);
if (!_exist) {
return this.initAuthInfo();
}
let { username, password } = payloads; let { username, password } = payloads;
const content = await this.getAuthInfo(); const content = await this.getAuthInfo();
const timestamp = Date.now(); const timestamp = Date.now();
if (content) { let {
let { username: cUsername,
username: cUsername, password: cPassword,
password: cPassword, retries = 0,
retries = 0, lastlogon,
lastlogon, lastip,
lastip, lastaddr,
lastaddr, twoFactorActivated,
twoFactorActivated, twoFactorActived,
twoFactorActived, tokens = {},
tokens = {}, platform,
platform, } = content;
} = content; // patch old field
// patch old field twoFactorActivated = twoFactorActivated || twoFactorActived;
twoFactorActivated = twoFactorActivated || twoFactorActived;
if ( const retriesTime = Math.pow(3, retries) * 1000;
(cUsername === 'admin' && cPassword === 'admin') || if (retries > 2 && timestamp - lastlogon < retriesTime) {
!cUsername || const waitTime = Math.ceil(
!cPassword (retriesTime - (timestamp - lastlogon)) / 1000,
) { );
return this.initAuthInfo(); return {
} code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
}
const retriesTime = Math.pow(3, retries) * 1000; if (
if (retries > 2 && timestamp - lastlogon < retriesTime) { username === cUsername &&
const waitTime = Math.ceil( password === cPassword &&
(retriesTime - (timestamp - lastlogon)) / 1000, twoFactorActivated &&
); needTwoFactor
) {
await this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
};
}
const ip = requestIp.getClientIp(req) || '';
const query = new IP2Region();
const ipAddress = query.search(ip);
let address = '';
if (ipAddress) {
const { country, province, city, isp } = ipAddress;
address = uniq([country, province, city, isp]).filter(Boolean).join(' ');
}
if (username === cUsername && password === cPassword) {
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
});
await this.updateAuthInfo(content, {
token,
tokens: {
...tokens,
[req.platform]: token,
},
lastlogon: timestamp,
retries: 0,
lastip: ip,
lastaddr: address,
platform: req.platform,
isTwoFactorChecking: false,
});
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
} ip地址 ${ip}`,
);
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.success,
},
});
this.getLoginLog();
return {
code: 200,
data: { token, lastip, lastaddr, lastlogon, retries, platform },
};
} else {
await this.updateAuthInfo(content, {
retries: retries + 1,
lastlogon: timestamp,
lastip: ip,
lastaddr: address,
platform: req.platform,
});
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
} ip地址 ${ip}`,
);
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.fail,
},
});
this.getLoginLog();
if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return { return {
code: 410, code: 410,
message: `失败次数过多,请${waitTime}秒后重试`, message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime, data: waitTime,
}; };
}
if (
username === cUsername &&
password === cPassword &&
twoFactorActivated &&
needTwoFactor
) {
this.updateAuthInfo(content, {
isTwoFactorChecking: true,
});
return {
code: 420,
message: '',
};
}
const ip = requestIp.getClientIp(req) || '';
const query = new IP2Region();
const ipAddress = query.search(ip);
let address = '';
if (ipAddress) {
const { country, province, city, isp } = ipAddress;
address = uniq([country, province, city, isp])
.filter(Boolean)
.join(' ');
}
if (username === cUsername && password === cPassword) {
const data = createRandomString(50, 100);
const expiration = twoFactorActivated ? 60 : 20;
let token = jwt.sign({ data }, config.secret as any, {
expiresIn: 60 * 60 * 24 * expiration,
algorithm: 'HS384',
});
this.updateAuthInfo(content, {
token,
tokens: {
...tokens,
[req.platform]: token,
},
lastlogon: timestamp,
retries: 0,
lastip: ip,
lastaddr: address,
platform: req.platform,
isTwoFactorChecking: false,
});
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
} ip地址 ${ip}`,
);
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.success,
},
});
this.getLoginLog();
return {
code: 200,
data: { token, lastip, lastaddr, lastlogon, retries, platform },
};
} else { } else {
this.updateAuthInfo(content, { return { code: 400, message: config.authError };
retries: retries + 1,
lastlogon: timestamp,
lastip: ip,
lastaddr: address,
platform: req.platform,
});
this.notificationService.notify(
'登录通知',
`你于${dayjs(timestamp).format('YYYY-MM-DD HH:mm:ss')}${address} ${
req.platform
} ip地址 ${ip}`,
);
await this.insertDb({
type: AuthDataType.loginLog,
info: {
timestamp,
address,
ip,
platform: req.platform,
status: LoginStatus.fail,
},
});
this.getLoginLog();
if (retries > 2) {
const waitTime = Math.round(Math.pow(3, retries + 1));
return {
code: 410,
message: `失败次数过多,请${waitTime}秒后重试`,
data: waitTime,
};
} else {
return { code: 400, message: config.authError };
}
} }
} else {
return this.initAuthInfo();
} }
} }
public async logout(platform: string): Promise<any> { public async logout(platform: string): Promise<any> {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { await this.updateAuthInfo(authInfo, {
token: '', token: '',
tokens: { ...authInfo.tokens, [platform]: '' }, tokens: { ...authInfo.tokens, [platform]: '' },
}); });
@ -257,35 +240,21 @@ export default class UserService {
return { code: 400, message: '密码不能设置为admin' }; return { code: 400, message: '密码不能设置为admin' };
} }
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { username, password }); await this.updateAuthInfo(authInfo, { username, password });
return { code: 200, message: '更新成功' }; return { code: 200, message: '更新成功' };
} }
public async updateAvatar(avatar: string) { public async updateAvatar(avatar: string) {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { avatar }); await this.updateAuthInfo(authInfo, { avatar });
return { code: 200, data: avatar, message: '更新成功' }; return { code: 200, data: avatar, message: '更新成功' };
} }
public async getUserInfo(): Promise<any> {
const authFileExist = await fileExist(config.authConfigFile);
if (!authFileExist) {
await createFile(
config.authConfigFile,
JSON.stringify({
username: 'admin',
password: 'admin',
}),
);
}
return await this.getAuthInfo();
}
public async initTwoFactor() { public async initTwoFactor() {
const secret = authenticator.generateSecret(); const secret = authenticator.generateSecret();
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret); const otpauth = authenticator.keyuri(authInfo.username, 'qinglong', secret);
this.updateAuthInfo(authInfo, { twoFactorSecret: secret }); await this.updateAuthInfo(authInfo, { twoFactorSecret: secret });
return { secret, url: otpauth }; return { secret, url: otpauth };
} }
@ -296,7 +265,7 @@ export default class UserService {
secret: authInfo.twoFactorSecret, secret: authInfo.twoFactorSecret,
}); });
if (isValid) { if (isValid) {
this.updateAuthInfo(authInfo, { twoFactorActivated: true }); await this.updateAuthInfo(authInfo, { twoFactorActivated: true });
} }
return isValid; return isValid;
} }
@ -322,7 +291,7 @@ export default class UserService {
return this.login({ username, password }, req, false); return this.login({ username, password }, req, false);
} else { } else {
const { ip, address } = await getNetIp(req); const { ip, address } = await getNetIp(req);
this.updateAuthInfo(authInfo, { await this.updateAuthInfo(authInfo, {
lastip: ip, lastip: ip,
lastaddr: address, lastaddr: address,
platform: req.platform, platform: req.platform,
@ -333,7 +302,7 @@ export default class UserService {
public async deactiveTwoFactor() { public async deactiveTwoFactor() {
const authInfo = await this.getAuthInfo(); const authInfo = await this.getAuthInfo();
this.updateAuthInfo(authInfo, { await this.updateAuthInfo(authInfo, {
twoFactorActivated: false, twoFactorActivated: false,
twoFactorActived: false, twoFactorActived: false,
twoFactorSecret: '', twoFactorSecret: '',
@ -341,16 +310,22 @@ export default class UserService {
return true; return true;
} }
private async getAuthInfo() { public async getAuthInfo() {
const content = await fs.readFile(config.authConfigFile, 'utf8'); const authInfo = await shareStore.getAuthInfo();
return safeJSONParse(content); if (authInfo) {
return authInfo;
}
const doc = await this.getDb({ type: AuthDataType.authConfig });
return (doc.info || {}) as AuthInfo;
} }
private async updateAuthInfo(authInfo: any, info: any) { private async updateAuthInfo(authInfo: any, info: any) {
await fs.writeFile( const result = { ...authInfo, ...info };
config.authConfigFile, await shareStore.updateAuthInfo(result);
JSON.stringify({ ...authInfo, ...info }), await this.updateAuthDb({
); type: AuthDataType.authConfig,
info: result,
});
} }
public async getNotificationMode(): Promise<NotificationInfo> { public async getNotificationMode(): Promise<NotificationInfo> {

34
back/shared/store.ts Normal file
View File

@ -0,0 +1,34 @@
import { AuthInfo } from '../data/system';
import { App } from '../data/open';
import Keyv from 'keyv';
import KeyvSqlite from '@keyv/sqlite';
import config from '../config';
import path from 'path';
export enum EKeyv {
'apps' = 'apps',
'authInfo' = 'authInfo',
}
export interface IKeyvStore {
apps: App[];
authInfo: AuthInfo;
}
const keyvSqlite = new KeyvSqlite(path.join(config.dbPath, 'keyv.sqlite'));
export const keyvStore = new Keyv<IKeyvStore>({ store: keyvSqlite });
export const shareStore = {
getAuthInfo() {
return keyvStore.get<IKeyvStore['authInfo']>(EKeyv.authInfo);
},
updateAuthInfo(value: IKeyvStore['authInfo']) {
return keyvStore.set<IKeyvStore['authInfo']>(EKeyv.authInfo, value);
},
getApps() {
return keyvStore.get<IKeyvStore['apps']>(EKeyv.apps);
},
updateApps(apps: App[]) {
return keyvStore.set<IKeyvStore['apps']>(EKeyv.apps, apps);
},
};

View File

@ -54,6 +54,9 @@
"react-dom": "18", "react-dom": "18",
"dva-core": "2" "dva-core": "2"
} }
},
"overrides": {
"sqlite3": "git+https://github.com/whyour/node-sqlite3.git#v1.0.3"
} }
}, },
"dependencies": { "dependencies": {
@ -98,7 +101,9 @@
"winston": "^3.17.0", "winston": "^3.17.0",
"winston-daily-rotate-file": "^5.0.0", "winston-daily-rotate-file": "^5.0.0",
"request-ip": "3.3.0", "request-ip": "3.3.0",
"ip2region": "2.3.0" "ip2region": "2.3.0",
"keyv": "^5.2.3",
"@keyv/sqlite": "^4.0.1"
}, },
"devDependencies": { "devDependencies": {
"moment": "2.30.1", "moment": "2.30.1",

View File

@ -1,13 +1,15 @@
lockfileVersion: '6.0' lockfileVersion: '6.0'
settings: overrides:
autoInstallPeers: true sqlite3: git+https://github.com/whyour/node-sqlite3.git#v1.0.3
excludeLinksFromLockfile: false
dependencies: dependencies:
'@grpc/grpc-js': '@grpc/grpc-js':
specifier: ^1.12.3 specifier: ^1.12.3
version: 1.12.3 version: 1.12.3
'@keyv/sqlite':
specifier: ^4.0.1
version: 4.0.1
'@otplib/preset-default': '@otplib/preset-default':
specifier: ^12.0.1 specifier: ^12.0.1
version: 12.0.1 version: 12.0.1
@ -74,6 +76,9 @@ dependencies:
jsonwebtoken: jsonwebtoken:
specifier: ^9.0.2 specifier: ^9.0.2
version: 9.0.2 version: 9.0.2
keyv:
specifier: ^5.2.3
version: 5.2.3
lodash: lodash:
specifier: ^4.17.21 specifier: ^4.17.21
version: 4.17.21 version: 4.17.21
@ -2773,6 +2778,23 @@ packages:
resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==} resolution: {integrity: sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==}
dev: false dev: false
/@keyv/serialize@1.0.2:
resolution: {integrity: sha512-+E/LyaAeuABniD/RvUezWVXKpeuvwLEA9//nE9952zBaOdBd2mQ3pPoM8cUe2X6IcMByfuSLzmYqnYshG60+HQ==}
dependencies:
buffer: 6.0.3
dev: false
/@keyv/sqlite@4.0.1:
resolution: {integrity: sha512-Ngs9jhElXN7efS9WvvCC/p6rXMbihna8eNLVBc421Zf+VcFd+pR4DOcS6yA9V22EKtAy4DEj7LtvEEIm0bb80A==}
engines: {node: '>= 18'}
dependencies:
sqlite3: github.com/whyour/node-sqlite3/3a00af0b5d7603b7f1b290032507320b18a6b741
transitivePeerDependencies:
- bluebird
- encoding
- supports-color
dev: false
/@lezer/common@1.2.3: /@lezer/common@1.2.3:
resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==} resolution: {integrity: sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==}
dev: true dev: true
@ -6377,7 +6399,6 @@ packages:
/base64-js@1.5.1: /base64-js@1.5.1:
resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
dev: true
/before@0.0.1: /before@0.0.1:
resolution: {integrity: sha512-1J5SWbkoVJH9DTALN8igB4p+nPKZzPrJ/HomqBDLpfUvDXCdjdBmBUcH5McZfur0lftVssVU6BZug5NYh87zTw==} resolution: {integrity: sha512-1J5SWbkoVJH9DTALN8igB4p+nPKZzPrJ/HomqBDLpfUvDXCdjdBmBUcH5McZfur0lftVssVU6BZug5NYh87zTw==}
@ -6572,6 +6593,13 @@ packages:
isarray: 1.0.0 isarray: 1.0.0
dev: true dev: true
/buffer@6.0.3:
resolution: {integrity: sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==}
dependencies:
base64-js: 1.5.1
ieee754: 1.2.1
dev: false
/builtin-status-codes@3.0.0: /builtin-status-codes@3.0.0:
resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==} resolution: {integrity: sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==}
dev: true dev: true
@ -9406,7 +9434,6 @@ packages:
/ieee754@1.2.1: /ieee754@1.2.1:
resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
dev: true
/ignore-by-default@1.0.1: /ignore-by-default@1.0.1:
resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==} resolution: {integrity: sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==}
@ -10161,6 +10188,12 @@ packages:
dependencies: dependencies:
json-buffer: 3.0.1 json-buffer: 3.0.1
/keyv@5.2.3:
resolution: {integrity: sha512-AGKecUfzrowabUv0bH1RIR5Vf7w+l4S3xtQAypKaUpTdIR1EbrAcTxHCrpo9Q+IWeUlFE2palRtgIQcgm+PQJw==}
dependencies:
'@keyv/serialize': 1.0.2
dev: false
/kind-of@6.0.3: /kind-of@6.0.3:
resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
@ -16141,3 +16174,7 @@ packages:
- encoding - encoding
- supports-color - supports-color
dev: false dev: false
settings:
autoInstallPeers: true
excludeLinksFromLockfile: false

View File

@ -193,12 +193,6 @@ fix_config() {
echo echo
fi fi
if [[ ! -s $file_auth_user ]]; then
echo -e "复制一份 $file_auth_sample$file_auth_user\n"
cp -fv $file_auth_sample $file_auth_user
echo
fi
if [[ ! -s $file_notify_py ]]; then if [[ ! -s $file_notify_py ]]; then
echo -e "复制一份 $file_notify_py_sample$file_notify_py\n" echo -e "复制一份 $file_notify_py_sample$file_notify_py\n"
cp -fv $file_notify_py_sample $file_notify_py cp -fv $file_notify_py_sample $file_notify_py