mirror of
https://github.com/chaos-zhu/easynode.git
synced 2026-10-06 14:23:19 +08:00
feat: 调整登录有效期选择
This commit is contained in:
@@ -1,36 +1,12 @@
|
|||||||
enum LoginExpiry { temporary, currentDay, threeDays, sevenDays }
|
enum LoginExpiry { threeDays, sevenDays, thirtyDays }
|
||||||
|
|
||||||
String jwtExpiresFor(LoginExpiry expiry, {DateTime? now}) {
|
String jwtExpiresFor(LoginExpiry expiry) {
|
||||||
switch (expiry) {
|
switch (expiry) {
|
||||||
case LoginExpiry.temporary:
|
|
||||||
return '1h';
|
|
||||||
case LoginExpiry.currentDay:
|
|
||||||
final current = now ?? DateTime.now();
|
|
||||||
final tomorrow = DateTime(current.year, current.month, current.day + 1);
|
|
||||||
final seconds = tomorrow.difference(current).inSeconds;
|
|
||||||
return '${seconds}s';
|
|
||||||
case LoginExpiry.threeDays:
|
case LoginExpiry.threeDays:
|
||||||
return '3d';
|
return '3d';
|
||||||
case LoginExpiry.sevenDays:
|
case LoginExpiry.sevenDays:
|
||||||
return '7d';
|
return '7d';
|
||||||
|
case LoginExpiry.thirtyDays:
|
||||||
|
return '30d';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int jwtExpireAtFor(LoginExpiry expiry, {DateTime? now}) {
|
|
||||||
final current = now ?? DateTime.now();
|
|
||||||
final expires = jwtExpiresFor(expiry, now: current);
|
|
||||||
final match = RegExp(r'^(\d+)([smhd])$').firstMatch(expires);
|
|
||||||
if (match == null) {
|
|
||||||
throw const FormatException('invalid jwt expiry');
|
|
||||||
}
|
|
||||||
final count = int.parse(match.group(1)!);
|
|
||||||
final unit = match.group(2)!;
|
|
||||||
final multiplier = switch (unit) {
|
|
||||||
's' => 1000,
|
|
||||||
'm' => 60 * 1000,
|
|
||||||
'h' => 60 * 60 * 1000,
|
|
||||||
'd' => 24 * 60 * 60 * 1000,
|
|
||||||
_ => 1000,
|
|
||||||
};
|
|
||||||
return current.millisecondsSinceEpoch + count * multiplier;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -63,8 +63,7 @@ class LoginController {
|
|||||||
required String mfa2Token,
|
required String mfa2Token,
|
||||||
required bool httpRiskAccepted,
|
required bool httpRiskAccepted,
|
||||||
required bool savePassword,
|
required bool savePassword,
|
||||||
LoginExpiry expiry = LoginExpiry.temporary,
|
LoginExpiry expiry = LoginExpiry.threeDays,
|
||||||
DateTime? now,
|
|
||||||
}) async {
|
}) async {
|
||||||
final String normalized;
|
final String normalized;
|
||||||
try {
|
try {
|
||||||
@@ -99,8 +98,7 @@ class LoginController {
|
|||||||
final response = await api.postJson('/login', {
|
final response = await api.postJson('/login', {
|
||||||
'loginName': username,
|
'loginName': username,
|
||||||
'ciphertext': ciphertext,
|
'ciphertext': ciphertext,
|
||||||
'jwtExpires': jwtExpiresFor(expiry, now: now),
|
'jwtExpires': jwtExpiresFor(expiry),
|
||||||
'jwtExpireAt': jwtExpireAtFor(expiry, now: now),
|
|
||||||
if (mfa2Token.isNotEmpty) 'mfa2Token': mfa2Token,
|
if (mfa2Token.isNotEmpty) 'mfa2Token': mfa2Token,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class _LoginPageState extends State<LoginPage> {
|
|||||||
final FocusNode _userFocus = FocusNode();
|
final FocusNode _userFocus = FocusNode();
|
||||||
final FocusNode _passwordFocus = FocusNode();
|
final FocusNode _passwordFocus = FocusNode();
|
||||||
|
|
||||||
LoginExpiry _expiry = LoginExpiry.currentDay;
|
LoginExpiry _expiry = LoginExpiry.threeDays;
|
||||||
bool _savePassword = false;
|
bool _savePassword = false;
|
||||||
bool _httpRiskAccepted = false;
|
bool _httpRiskAccepted = false;
|
||||||
bool _submitting = false;
|
bool _submitting = false;
|
||||||
@@ -564,9 +564,9 @@ class _ExpiryPicker extends StatelessWidget {
|
|||||||
final ValueChanged<LoginExpiry> onChanged;
|
final ValueChanged<LoginExpiry> onChanged;
|
||||||
|
|
||||||
static const _optionKeys = <LoginExpiry, String>{
|
static const _optionKeys = <LoginExpiry, String>{
|
||||||
LoginExpiry.currentDay: 'login.expiry.currentDay',
|
|
||||||
LoginExpiry.threeDays: 'login.expiry.threeDays',
|
LoginExpiry.threeDays: 'login.expiry.threeDays',
|
||||||
LoginExpiry.sevenDays: 'login.expiry.sevenDays',
|
LoginExpiry.sevenDays: 'login.expiry.sevenDays',
|
||||||
|
LoginExpiry.thirtyDays: 'login.expiry.thirtyDays',
|
||||||
};
|
};
|
||||||
|
|
||||||
@override
|
@override
|
||||||
|
|||||||
@@ -49,10 +49,9 @@ const Map<String, String> stringsEn = {
|
|||||||
'login.errMissingFields': 'Server login response is missing required fields',
|
'login.errMissingFields': 'Server login response is missing required fields',
|
||||||
|
|
||||||
// Login expiry options
|
// Login expiry options
|
||||||
'login.expiry.temporary': 'Temporary, 1 hour',
|
|
||||||
'login.expiry.currentDay': 'Today',
|
|
||||||
'login.expiry.threeDays': '3 days',
|
'login.expiry.threeDays': '3 days',
|
||||||
'login.expiry.sevenDays': '7 days',
|
'login.expiry.sevenDays': '7 days',
|
||||||
|
'login.expiry.thirtyDays': '30 days',
|
||||||
|
|
||||||
// Main shell tabs
|
// Main shell tabs
|
||||||
'tabs.servers': 'Servers',
|
'tabs.servers': 'Servers',
|
||||||
|
|||||||
@@ -46,10 +46,9 @@ const Map<String, String> stringsZh = {
|
|||||||
'login.errSchemeUnsupported': '服务端地址仅支持 http 或 https',
|
'login.errSchemeUnsupported': '服务端地址仅支持 http 或 https',
|
||||||
'login.errLoginGeneric': '登录失败',
|
'login.errLoginGeneric': '登录失败',
|
||||||
'login.errMissingFields': '服务端登录响应缺少必要字段',
|
'login.errMissingFields': '服务端登录响应缺少必要字段',
|
||||||
'login.expiry.temporary': '临时会话,1 小时',
|
'login.expiry.threeDays': '3天',
|
||||||
'login.expiry.currentDay': '今天',
|
'login.expiry.sevenDays': '7天',
|
||||||
'login.expiry.threeDays': '3 天',
|
'login.expiry.thirtyDays': '30天',
|
||||||
'login.expiry.sevenDays': '7 天',
|
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
'tabs.servers': '服务器',
|
'tabs.servers': '服务器',
|
||||||
|
|||||||
@@ -2,23 +2,12 @@ import 'package:flutter_test/flutter_test.dart';
|
|||||||
import 'package:easynode_native/core/utils/jwt_expiry.dart';
|
import 'package:easynode_native/core/utils/jwt_expiry.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
test('maps temporary expiry to one hour', () {
|
|
||||||
expect(jwtExpiresFor(LoginExpiry.temporary), '1h');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('maps three days and seven days', () {
|
test('maps three days and seven days', () {
|
||||||
expect(jwtExpiresFor(LoginExpiry.threeDays), '3d');
|
expect(jwtExpiresFor(LoginExpiry.threeDays), '3d');
|
||||||
expect(jwtExpiresFor(LoginExpiry.sevenDays), '7d');
|
expect(jwtExpiresFor(LoginExpiry.sevenDays), '7d');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('maps current day to remaining seconds until midnight', () {
|
test('maps thirty days', () {
|
||||||
final fixed = DateTime(2026, 5, 16, 23, 59, 30);
|
expect(jwtExpiresFor(LoginExpiry.thirtyDays), '30d');
|
||||||
expect(jwtExpiresFor(LoginExpiry.currentDay, now: fixed), '30s');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('jwtExpireAtFor returns milliseconds advanced by the duration', () {
|
|
||||||
final fixed = DateTime.utc(2026, 5, 16, 0, 0, 0);
|
|
||||||
final expectedMs = fixed.millisecondsSinceEpoch + 60 * 60 * 1000;
|
|
||||||
expect(jwtExpireAtFor(LoginExpiry.temporary, now: fixed), expectedMs);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,13 @@ const sessionDB = new SessionDB().getInstance()
|
|||||||
const plusDB = new PlusDB().getInstance()
|
const plusDB = new PlusDB().getInstance()
|
||||||
const runtimeState = new RuntimeState().getInstance()
|
const runtimeState = new RuntimeState().getInstance()
|
||||||
|
|
||||||
|
// 仅允许三种登录有效期:3天 / 7天 / 30天
|
||||||
|
const ALLOWED_JWT_EXPIRES = {
|
||||||
|
'3d': 3 * 24 * 60 * 60 * 1000,
|
||||||
|
'7d': 7 * 24 * 60 * 60 * 1000,
|
||||||
|
'30d': 30 * 24 * 60 * 60 * 1000
|
||||||
|
}
|
||||||
|
|
||||||
const getpublicKey = async ({ res }) => {
|
const getpublicKey = async ({ res }) => {
|
||||||
let { publicKey: data } = await keyDB.findOneAsync({})
|
let { publicKey: data } = await keyDB.findOneAsync({})
|
||||||
if (!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
|
if (!data) return res.fail({ msg: 'publicKey not found, Try to restart the server', status: 500 })
|
||||||
@@ -46,8 +53,11 @@ let forbidLogin = false
|
|||||||
|
|
||||||
const login = async (ctx) => {
|
const login = async (ctx) => {
|
||||||
const { res, request } = ctx
|
const { res, request } = ctx
|
||||||
let { body: { loginName, ciphertext, jwtExpires, jwtExpireAt, mfa2Token }, ip: clientIp, header } = request
|
let { body: { loginName, ciphertext, jwtExpires, mfa2Token }, ip: clientIp, header } = request
|
||||||
if (!loginName || !ciphertext || !jwtExpires || !jwtExpireAt || !header) return res.fail({ msg: '请求非法!' })
|
if (!loginName || !ciphertext || !jwtExpires || !header) return res.fail({ msg: '请求非法!' })
|
||||||
|
const jwtExpiresDuration = ALLOWED_JWT_EXPIRES[jwtExpires]
|
||||||
|
if (typeof jwtExpiresDuration !== 'number') return res.fail({ msg: '请求非法!' })
|
||||||
|
const jwtExpireAt = Date.now() + jwtExpiresDuration
|
||||||
if (forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` })
|
if (forbidLogin) return res.fail({ msg: `禁止登录! 倒计时[${ loginCountDown }s]后尝试登录或重启面板服务` })
|
||||||
loginErrCount++
|
loginErrCount++
|
||||||
loginErrTotal++
|
loginErrTotal++
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const useStore = defineStore('global', {
|
|||||||
localScriptList: [],
|
localScriptList: [],
|
||||||
suspendedSessions: [], // 挂起的会话列表
|
suspendedSessions: [], // 挂起的会话列表
|
||||||
user: localStorage.getItem('user') || null,
|
user: localStorage.getItem('user') || null,
|
||||||
token: localStorage.getItem('token') || sessionStorage.getItem('token') || null,
|
token: localStorage.getItem('token') || null,
|
||||||
deviceId: localStorage.getItem('deviceId') || null,
|
deviceId: localStorage.getItem('deviceId') || null,
|
||||||
title: '',
|
title: '',
|
||||||
isDark: false,
|
isDark: false,
|
||||||
@@ -82,9 +82,8 @@ const useStore = defineStore('global', {
|
|||||||
chatHistory: []
|
chatHistory: []
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async setJwtToken(token, isSession = true) {
|
async setJwtToken(token) {
|
||||||
if (isSession) sessionStorage.setItem('token', token)
|
localStorage.setItem('token', token)
|
||||||
else localStorage.setItem('token', token)
|
|
||||||
this.$patch({ token })
|
this.$patch({ token })
|
||||||
},
|
},
|
||||||
async setUser(username, deviceId) {
|
async setUser(username, deviceId) {
|
||||||
@@ -104,8 +103,6 @@ const useStore = defineStore('global', {
|
|||||||
console.error('注销登录凭证失败: ', err.message)
|
console.error('注销登录凭证失败: ', err.message)
|
||||||
} finally {
|
} finally {
|
||||||
localStorage.removeItem('token')
|
localStorage.removeItem('token')
|
||||||
sessionStorage.removeItem('token')
|
|
||||||
sessionStorage.removeItem('uid')
|
|
||||||
localStorage.removeItem('uid')
|
localStorage.removeItem('uid')
|
||||||
localStorage.removeItem('user')
|
localStorage.removeItem('user')
|
||||||
localStorage.removeItem('deviceId')
|
localStorage.removeItem('deviceId')
|
||||||
|
|||||||
+1
-18
@@ -37,23 +37,6 @@ export const randomStr = (e) =>{
|
|||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
|
|
||||||
export const jwtExpireToTimestamp = (str) => {
|
|
||||||
const match = /^(\d+)([smhd])$/.exec(str)
|
|
||||||
if (!match) throw new Error('jwt过期格式错误,必须是 [xx]s / [xx]h / [xx]d')
|
|
||||||
|
|
||||||
const n = Number(match[1])
|
|
||||||
const unit = match[2]
|
|
||||||
|
|
||||||
const map = {
|
|
||||||
s: 1000,
|
|
||||||
m: 60 * 1000,
|
|
||||||
h: 60 * 60 * 1000,
|
|
||||||
d: 24 * 60 * 60 * 1000
|
|
||||||
}
|
|
||||||
|
|
||||||
return n * map[unit] + Date.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
// rsa公钥加密
|
// rsa公钥加密
|
||||||
export const RSAEncrypt = (text) => {
|
export const RSAEncrypt = (text) => {
|
||||||
const publicKey = localStorage.getItem('publicKey')
|
const publicKey = localStorage.getItem('publicKey')
|
||||||
@@ -210,4 +193,4 @@ export const generateSocketInstance = (path, config = { forceNew: false, reconne
|
|||||||
},
|
},
|
||||||
...config
|
...config
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -58,10 +58,9 @@
|
|||||||
</el-form-item>
|
</el-form-item>
|
||||||
<el-form-item prop="jwtExpires" label="登录有效期">
|
<el-form-item prop="jwtExpires" label="登录有效期">
|
||||||
<el-radio-group v-model="expireTime" class="login-indate">
|
<el-radio-group v-model="expireTime" class="login-indate">
|
||||||
<el-radio :value="expireEnum.ONE_SESSION">临时</el-radio>
|
<el-radio :value="expireEnum.THREE_DAY">3天</el-radio>
|
||||||
<el-radio :value="expireEnum.CURRENT_DAY">当天</el-radio>
|
<el-radio :value="expireEnum.SEVEN_DAY">7天</el-radio>
|
||||||
<el-radio :value="expireEnum.THREE_DAY">三天</el-radio>
|
<el-radio :value="expireEnum.THIRTY_DAY">30天</el-radio>
|
||||||
<el-radio :value="expireEnum.SEVEN_DAY">七天</el-radio>
|
|
||||||
</el-radio-group>
|
</el-radio-group>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
@@ -83,19 +82,18 @@
|
|||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
|
import { ref, reactive, onMounted, getCurrentInstance } from 'vue'
|
||||||
import { RSAEncrypt, jwtExpireToTimestamp } from '@utils/index.js'
|
import { RSAEncrypt } from '@utils/index.js'
|
||||||
import Warn from './warn.vue'
|
import Warn from './warn.vue'
|
||||||
import { version } from '../../../package.json'
|
import { version } from '../../../package.json'
|
||||||
|
|
||||||
const { proxy: { $store, $api, $message, $router, $messageBox } } = getCurrentInstance()
|
const { proxy: { $store, $api, $message, $router, $messageBox } } = getCurrentInstance()
|
||||||
|
|
||||||
const expireEnum = reactive({
|
const expireEnum = reactive({
|
||||||
ONE_SESSION: 'one_session',
|
|
||||||
CURRENT_DAY: 'current_day',
|
|
||||||
THREE_DAY: 'three_day',
|
THREE_DAY: 'three_day',
|
||||||
SEVEN_DAY: 'seven_day'
|
SEVEN_DAY: 'seven_day',
|
||||||
|
THIRTY_DAY: 'thirty_day'
|
||||||
})
|
})
|
||||||
const expireTime = ref(expireEnum.CURRENT_DAY)
|
const expireTime = ref(expireEnum.THREE_DAY)
|
||||||
const loginFormRefs = ref(null)
|
const loginFormRefs = ref(null)
|
||||||
const notKey = ref(false)
|
const notKey = ref(false)
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
@@ -104,7 +102,6 @@ const notHttpsTips = localStorage.getItem('notHttpsTips') === 'true'
|
|||||||
const loginForm = reactive({
|
const loginForm = reactive({
|
||||||
loginName: '',
|
loginName: '',
|
||||||
pwd: '',
|
pwd: '',
|
||||||
jwtExpires: 1,
|
|
||||||
mfa2Token: ''
|
mfa2Token: ''
|
||||||
})
|
})
|
||||||
const rules = reactive({
|
const rules = reactive({
|
||||||
@@ -115,29 +112,26 @@ const rules = reactive({
|
|||||||
|
|
||||||
const handleLogin = () => {
|
const handleLogin = () => {
|
||||||
loginFormRefs.value.validate().then(async () => {
|
loginFormRefs.value.validate().then(async () => {
|
||||||
let { jwtExpires, loginName, pwd, mfa2Token } = loginForm
|
const { loginName, pwd, mfa2Token } = loginForm
|
||||||
|
let jwtExpires = '3d'
|
||||||
switch (expireTime.value) {
|
switch (expireTime.value) {
|
||||||
case expireEnum.ONE_SESSION:
|
|
||||||
jwtExpires = '1h' // 会话登录token1小时有效期,浏览器窗口关闭则立即失效
|
|
||||||
break
|
|
||||||
case expireEnum.CURRENT_DAY:
|
|
||||||
jwtExpires = `${ Math.floor((new Date().setHours(24,0,0,0) - Date.now()) / 1000) }s`
|
|
||||||
break
|
|
||||||
case expireEnum.THREE_DAY:
|
case expireEnum.THREE_DAY:
|
||||||
jwtExpires = '3d'
|
jwtExpires = '3d'
|
||||||
break
|
break
|
||||||
case expireEnum.SEVEN_DAY:
|
case expireEnum.SEVEN_DAY:
|
||||||
jwtExpires = '7d'
|
jwtExpires = '7d'
|
||||||
break
|
break
|
||||||
|
case expireEnum.THIRTY_DAY:
|
||||||
|
jwtExpires = '30d'
|
||||||
|
break
|
||||||
}
|
}
|
||||||
const jwtExpireAt = jwtExpireToTimestamp(jwtExpires)
|
|
||||||
const ciphertext = RSAEncrypt(pwd)
|
const ciphertext = RSAEncrypt(pwd)
|
||||||
if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
|
if (ciphertext === -1) return $message.error({ message: '公钥加载失败', center: true })
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, jwtExpireAt, mfa2Token })
|
let { data, msg } = await $api.login({ loginName, ciphertext, jwtExpires, mfa2Token })
|
||||||
const { token, deviceId } = data
|
const { token, deviceId } = data
|
||||||
$store.setJwtToken(token, expireEnum.ONE_SESSION === expireTime.value)
|
$store.setJwtToken(token)
|
||||||
$store.setUser(loginName, deviceId)
|
$store.setUser(loginName, deviceId)
|
||||||
$message.success({ message: msg || 'success', center: true })
|
$message.success({ message: msg || 'success', center: true })
|
||||||
if (isHttps) return $router.push('/')
|
if (isHttps) return $router.push('/')
|
||||||
|
|||||||
Reference in New Issue
Block a user