Files
AstrBot/dashboard/src/stores/auth.ts
Weilong Liao 7ddf6371b9 fix(webui): enforce 12-char dashboard password policy with backend+frontend validation (#7338)
* fix(webui): enforce 12-char dashboard password policy with backend+frontend validation

* fix(i18n): update password policy hints and validation rules for improved security

* test: adapt dashboard auth fixtures for hashed default password

* fix(security): increase PBKDF2 iterations

* feat(auth): implement secure login challenge and proof verification

* chore: ruff format

* fix(auth): update md5 import syntax for consistency

* feat(dashboard): implement random password generation for empty dashboard password

* feat(auth): enforce plaintext password requirement for legacy MD5 hashes

* fix(i18n): update password hint texts to reflect auto-generated initial passwords

* feat(dashboard): implement password change requirement and reset logic

* feat(auth): implement account setup flow and password change requirements

* feat: Implement legacy password support and upgrade mechanism

- Added `hash_legacy_dashboard_password` function for MD5 hashing of passwords.
- Updated configuration handling to store both PBKDF2 and legacy password hashes.
- Introduced logic to check if password storage has been upgraded and if a password change is required.
- Modified dashboard authentication routes to handle legacy password checks and prompts for upgrades.
- Updated frontend to display warnings for legacy password storage and required upgrades.
- Enhanced tests to cover scenarios for legacy password handling and migration to new storage format.

* fix(logo): update text color styles to use CSS variables for consistency

* feat(dashboard): upgrade password storage and enforce change requirement

* fix(dashboard): update minimum password length from 12 to 10 characters

* fix(dashboard): update minimum password length from 10 to 8 characters

---------

Co-authored-by: RC-CHN <1051989940@qq.com>
2026-05-12 23:46:57 +08:00

127 lines
4.3 KiB
TypeScript

import { defineStore } from 'pinia';
import { router } from '@/router';
import axios from 'axios';
export const useAuthStore = defineStore("auth", {
state: () => ({
// @ts-ignore
username: '',
returnUrl: null
}),
actions: {
async finishAuthenticatedSession(data: any): Promise<void> {
this.username = data.username;
localStorage.setItem('user', this.username);
localStorage.setItem('token', data.token);
const passwordUpgradeRequired = !!data?.password_upgrade_required;
const passwordWarning =
!!data?.change_pwd_hint ||
(!!data?.legacy_pwd_hint && !passwordUpgradeRequired);
if (passwordWarning) {
localStorage.setItem('change_pwd_hint', 'true');
if (data?.legacy_pwd_hint && !passwordUpgradeRequired) {
localStorage.setItem('legacy_pwd_hint', 'true');
} else {
localStorage.removeItem('legacy_pwd_hint');
}
} else {
localStorage.removeItem('change_pwd_hint');
localStorage.removeItem('legacy_pwd_hint');
}
if (passwordUpgradeRequired) {
localStorage.setItem('password_upgrade_required', 'true');
} else {
localStorage.removeItem('password_upgrade_required');
}
const onboardingCompleted = await this.checkOnboardingCompleted();
this.returnUrl = null;
if (passwordWarning) {
router.push('/auth/setup');
return;
}
if (onboardingCompleted) {
router.push('/dashboard/default');
} else {
router.push('/welcome');
}
},
async login(username: string, password: string): Promise<void> {
try {
const res = await axios.post('/api/auth/login', {
username: username,
password: password
});
if (res.data.status === 'error') {
return Promise.reject(res.data.message);
}
await this.finishAuthenticatedSession(res.data.data);
} catch (error) {
return Promise.reject(error);
}
},
async setup(username: string, password: string, confirmPassword: string): Promise<void> {
try {
const setupEndpoint = this.has_token() ? '/api/auth/setup-authenticated' : '/api/auth/setup';
const res = await axios.post(setupEndpoint, {
username: username,
password: password,
confirm_password: confirmPassword
});
if (res.data.status === 'error') {
return Promise.reject(res.data.message);
}
await this.finishAuthenticatedSession(res.data.data);
} catch (error) {
return Promise.reject(error);
}
},
async checkOnboardingCompleted(): Promise<boolean> {
try {
// 1. 检查平台配置
const platformRes = await axios.get('/api/config/get');
const hasPlatform = (platformRes.data.data.config.platform || []).length > 0;
if (!hasPlatform) return false;
// 2. 检查提供者配置
const providerRes = await axios.get('/api/config/provider/template');
const providers = providerRes.data.data?.providers || [];
const sources = providerRes.data.data?.provider_sources || [];
const sourceMap = new Map();
sources.forEach((s: any) => sourceMap.set(s.id, s.provider_type));
const hasProvider = providers.some((provider: any) => {
if (provider.provider_type) return provider.provider_type === 'chat_completion';
if (provider.provider_source_id) {
const type = sourceMap.get(provider.provider_source_id);
if (type === 'chat_completion') return true;
}
return String(provider.type || '').includes('chat_completion');
});
return hasProvider;
} catch (e) {
console.error('Failed to check onboarding status:', e);
return false;
}
},
logout() {
this.username = '';
localStorage.removeItem('user');
localStorage.removeItem('token');
localStorage.removeItem('change_pwd_hint');
localStorage.removeItem('legacy_pwd_hint');
localStorage.removeItem('password_upgrade_required');
void axios.post('/api/auth/logout').catch(() => undefined);
router.push('/auth/login');
},
has_token(): boolean {
return !!localStorage.getItem('token');
}
}
});