mirror of
https://github.com/EasyTier/EasyTier.git
synced 2026-05-13 17:35:37 +00:00
feat(web): warn on default-password accounts
Track built-in admin and user accounts that still use their seeded password so the web UI can prompt operators to rotate credentials after deployment. - Persist must-change-password state for seeded accounts. - Clear the reminder after password changes and validate empty-password updates. - Keep the migration and auth API behavior explicit.
This commit is contained in:
@@ -1,17 +1,52 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, inject, ref } from 'vue';
|
||||
import { Card, Password, Button } from 'primevue';
|
||||
import { useToast } from 'primevue/usetoast';
|
||||
import { useRouter } from 'vue-router';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import ApiClient from '../modules/api';
|
||||
import { clearMustChangePasswordFlag } from '../modules/auth-status';
|
||||
|
||||
const dialogRef = inject<any>('dialogRef');
|
||||
|
||||
const api = computed<ApiClient>(() => dialogRef.value.data.api);
|
||||
|
||||
const password = ref('');
|
||||
const toast = useToast();
|
||||
const router = useRouter();
|
||||
const { t } = useI18n();
|
||||
const passwordIsEmpty = computed(() => password.value.trim().length === 0);
|
||||
|
||||
const changePassword = async () => {
|
||||
await api.value.change_password(password.value);
|
||||
dialogRef.value.close();
|
||||
if (passwordIsEmpty.value) {
|
||||
toast.add({
|
||||
severity: 'warn',
|
||||
summary: t('web.common.warning'),
|
||||
detail: t('web.settings.new_password_empty'),
|
||||
life: 3000,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.value.change_password(password.value);
|
||||
toast.add({
|
||||
severity: 'success',
|
||||
summary: t('web.common.success'),
|
||||
detail: t('web.main.password_changed_relogin'),
|
||||
life: 3000,
|
||||
});
|
||||
clearMustChangePasswordFlag();
|
||||
dialogRef.value.close();
|
||||
router.push({ name: 'login' });
|
||||
} catch (error) {
|
||||
toast.add({
|
||||
severity: 'error',
|
||||
summary: t('web.common.error'),
|
||||
detail: error instanceof Error ? error.message : String(error),
|
||||
life: 3000,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -19,15 +54,17 @@ const changePassword = async () => {
|
||||
<div class="flex items-center justify-center">
|
||||
<Card class="w-full max-w-md p-6">
|
||||
<template #header>
|
||||
<h2 class="text-2xl font-semibold text-center">Change Password
|
||||
<h2 class="text-2xl font-semibold text-center">{{ t('web.main.change_password') }}
|
||||
</h2>
|
||||
</template>
|
||||
<template #content>
|
||||
<div class="flex flex-col space-y-4">
|
||||
<Password v-model="password" placeholder="New Password" :feedback="false" toggleMask />
|
||||
<Button @click="changePassword" label="Ok" />
|
||||
<Password v-model="password" :placeholder="t('web.settings.new_password')" :feedback="false"
|
||||
toggleMask />
|
||||
<Button @click="changePassword" :label="t('web.common.confirm')"
|
||||
:disabled="passwordIsEmpty" />
|
||||
</div>
|
||||
</template>
|
||||
</Card>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
@@ -7,6 +7,7 @@ import { I18nUtils } from 'easytier-frontend-lib';
|
||||
import { getInitialApiHost, cleanAndLoadApiHosts, saveApiHost } from "../modules/api-host"
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient, { Credential, RegisterData } from '../modules/api';
|
||||
import { setMustChangePasswordFlag } from '../modules/auth-status';
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -33,6 +34,7 @@ const onSubmit = async () => {
|
||||
let ret = await api.value?.login(credential);
|
||||
if (ret.success) {
|
||||
localStorage.setItem('apiHost', btoa(apiHost.value));
|
||||
setMustChangePasswordFlag(Boolean(ret.mustChangePassword));
|
||||
router.push({
|
||||
name: 'dashboard',
|
||||
params: { apiHost: btoa(apiHost.value) },
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { I18nUtils } from 'easytier-frontend-lib'
|
||||
import { computed, onMounted, ref, onUnmounted, nextTick } from 'vue';
|
||||
import { Button, TieredMenu } from 'primevue';
|
||||
import { Button, Message, TieredMenu } from 'primevue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { useDialog } from 'primevue/usedialog';
|
||||
import ChangePassword from './ChangePassword.vue';
|
||||
import Icon from '../assets/easytier.png'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import ApiClient from '../modules/api';
|
||||
import {
|
||||
clearMustChangePasswordFlag,
|
||||
getMustChangePasswordFlag,
|
||||
setMustChangePasswordFlag,
|
||||
} from '../modules/auth-status';
|
||||
|
||||
const { t } = useI18n()
|
||||
const route = useRoute();
|
||||
@@ -15,6 +20,7 @@ const router = useRouter();
|
||||
const api = computed<ApiClient | undefined>(() => {
|
||||
try {
|
||||
return new ApiClient(atob(route.params.apiHost as string), () => {
|
||||
clearMustChangePasswordFlag();
|
||||
router.push({ name: 'login' });
|
||||
})
|
||||
} catch (e) {
|
||||
@@ -23,25 +29,42 @@ const api = computed<ApiClient | undefined>(() => {
|
||||
});
|
||||
|
||||
const dialog = useDialog();
|
||||
const mustChangePassword = ref(false);
|
||||
|
||||
const openChangePasswordDialog = () => {
|
||||
dialog.open(ChangePassword, {
|
||||
props: {
|
||||
modal: true,
|
||||
},
|
||||
data: {
|
||||
api: api.value,
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const loadAuthStatus = async () => {
|
||||
const cachedStatus = getMustChangePasswordFlag();
|
||||
if (cachedStatus !== null) {
|
||||
mustChangePassword.value = cachedStatus;
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await api.value?.check_login_status();
|
||||
mustChangePassword.value = Boolean(
|
||||
status?.loggedIn && status?.mustChangePassword,
|
||||
);
|
||||
setMustChangePasswordFlag(mustChangePassword.value);
|
||||
} catch (e) {
|
||||
console.error('Failed to load auth status', e);
|
||||
}
|
||||
};
|
||||
|
||||
const userMenu = ref();
|
||||
const userMenuItems = ref([
|
||||
{
|
||||
label: t('web.main.change_password'),
|
||||
icon: 'pi pi-key',
|
||||
command: () => {
|
||||
console.log('File');
|
||||
let ret = dialog.open(ChangePassword, {
|
||||
props: {
|
||||
modal: true,
|
||||
},
|
||||
data: {
|
||||
api: api.value,
|
||||
}
|
||||
});
|
||||
|
||||
console.log("return", ret)
|
||||
},
|
||||
command: openChangePasswordDialog,
|
||||
},
|
||||
{
|
||||
label: t('web.main.logout'),
|
||||
@@ -52,6 +75,7 @@ const userMenuItems = ref([
|
||||
} catch (e) {
|
||||
console.error("logout failed", e);
|
||||
}
|
||||
clearMustChangePasswordFlag();
|
||||
router.push({ name: 'login' });
|
||||
},
|
||||
},
|
||||
@@ -92,6 +116,7 @@ onMounted(async () => {
|
||||
// 等待 DOM 渲染完成后添加事件监听器
|
||||
await nextTick();
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
await loadAuthStatus();
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -171,6 +196,13 @@ onUnmounted(() => {
|
||||
<div class="p-4 sm:ml-64">
|
||||
<div class="p-4 border-2 border-gray-200 border-dashed rounded-lg dark:border-gray-700">
|
||||
<div class="grid grid-cols-1 gap-4">
|
||||
<Message v-if="mustChangePassword" severity="warn" :closable="false">
|
||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span>{{ t('web.main.default_password_warning') }}</span>
|
||||
<Button size="small" icon="pi pi-key" :label="t('web.main.change_password_now')"
|
||||
@click="openChangePasswordDialog" />
|
||||
</div>
|
||||
</Message>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" :api="api" />
|
||||
</RouterView>
|
||||
|
||||
@@ -2,6 +2,8 @@ import axios, { AxiosError, AxiosInstance, AxiosResponse, InternalAxiosRequestCo
|
||||
import { type Api, NetworkTypes, Utils } from 'easytier-frontend-lib';
|
||||
import { Md5 } from 'ts-md5';
|
||||
|
||||
const hashAuthPassword = (password: string) => Md5.hashStr(password);
|
||||
|
||||
export interface ValidateConfigResponse {
|
||||
toml_config: string;
|
||||
}
|
||||
@@ -14,6 +16,16 @@ export interface OidcConfigResponse {
|
||||
export interface LoginResponse {
|
||||
success: boolean;
|
||||
message: string;
|
||||
mustChangePassword?: boolean;
|
||||
}
|
||||
|
||||
export interface AuthStatusResponse {
|
||||
must_change_password: boolean;
|
||||
}
|
||||
|
||||
export interface CheckLoginStatusResponse {
|
||||
loggedIn: boolean;
|
||||
mustChangePassword: boolean;
|
||||
}
|
||||
|
||||
export interface RegisterResponse {
|
||||
@@ -82,7 +94,6 @@ export class ApiClient {
|
||||
|
||||
// 添加响应拦截器
|
||||
this.client.interceptors.response.use((response: AxiosResponse) => {
|
||||
console.debug('Axios Response:', response);
|
||||
return response.data; // 假设服务器返回的数据都在data属性中
|
||||
}, (error: any) => {
|
||||
if (error.response) {
|
||||
@@ -108,9 +119,8 @@ export class ApiClient {
|
||||
// 注册
|
||||
public async register(data: RegisterData): Promise<RegisterResponse> {
|
||||
try {
|
||||
data.credentials.password = Md5.hashStr(data.credentials.password);
|
||||
const response = await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
console.log("register response:", response);
|
||||
data.credentials.password = hashAuthPassword(data.credentials.password);
|
||||
await this.client.post<RegisterResponse>('/auth/register', data);
|
||||
return { success: true, message: 'Register success', };
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
@@ -123,10 +133,13 @@ export class ApiClient {
|
||||
// 登录
|
||||
public async login(data: Credential): Promise<LoginResponse> {
|
||||
try {
|
||||
data.password = Md5.hashStr(data.password);
|
||||
const response = await this.client.post<any>('/auth/login', data);
|
||||
console.log("login response:", response);
|
||||
return { success: true, message: 'Login success', };
|
||||
data.password = hashAuthPassword(data.password);
|
||||
const response = await this.client.post<any, AuthStatusResponse>('/auth/login', data);
|
||||
return {
|
||||
success: true,
|
||||
message: 'Login success',
|
||||
mustChangePassword: response.must_change_password,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof AxiosError) {
|
||||
if (error.response?.status === 401) {
|
||||
@@ -147,16 +160,15 @@ export class ApiClient {
|
||||
}
|
||||
|
||||
public async change_password(new_password: string) {
|
||||
await this.client.put('/auth/password', { new_password: Md5.hashStr(new_password) });
|
||||
await this.client.put('/auth/password', { new_password: hashAuthPassword(new_password) });
|
||||
}
|
||||
|
||||
public async check_login_status() {
|
||||
try {
|
||||
await this.client.get('/auth/check_login_status');
|
||||
return true;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
public async check_login_status(): Promise<CheckLoginStatusResponse> {
|
||||
const response = await this.client.get<any, AuthStatusResponse>('/auth/check_login_status');
|
||||
return {
|
||||
loggedIn: true,
|
||||
mustChangePassword: response.must_change_password,
|
||||
};
|
||||
}
|
||||
|
||||
public async list_session() {
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
const MUST_CHANGE_PASSWORD_STORAGE_KEY = 'auth.mustChangePassword';
|
||||
|
||||
export const getMustChangePasswordFlag = (): boolean | null => {
|
||||
const value = sessionStorage.getItem(MUST_CHANGE_PASSWORD_STORAGE_KEY);
|
||||
if (value === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return value === 'true';
|
||||
};
|
||||
|
||||
export const setMustChangePasswordFlag = (value: boolean) => {
|
||||
sessionStorage.setItem(MUST_CHANGE_PASSWORD_STORAGE_KEY, value ? 'true' : 'false');
|
||||
};
|
||||
|
||||
export const clearMustChangePasswordFlag = () => {
|
||||
sessionStorage.removeItem(MUST_CHANGE_PASSWORD_STORAGE_KEY);
|
||||
};
|
||||
Reference in New Issue
Block a user