Add default credential reset safeguards
This commit is contained in:
@@ -10,6 +10,7 @@ import { useKeyboard } from '@/composables/useKeyboard'
|
||||
import { useAppStore } from '@/stores/hermes/app'
|
||||
import SessionSearchModal from '@/components/hermes/chat/SessionSearchModal.vue'
|
||||
import AuthEventListener from '@/components/auth/AuthEventListener.vue'
|
||||
import DefaultCredentialPrompt from '@/components/auth/DefaultCredentialPrompt.vue'
|
||||
|
||||
const { isDark, isComic } = useTheme()
|
||||
const { t } = useI18n()
|
||||
@@ -73,6 +74,7 @@ useKeyboard()
|
||||
</main>
|
||||
</div>
|
||||
<SessionSearchModal />
|
||||
<DefaultCredentialPrompt />
|
||||
</NNotificationProvider>
|
||||
</NDialogProvider>
|
||||
</NMessageProvider>
|
||||
|
||||
@@ -36,6 +36,7 @@ export interface CurrentUser {
|
||||
created_at: number
|
||||
updated_at: number
|
||||
last_login_at: number | null
|
||||
requiresCredentialChange?: boolean
|
||||
}
|
||||
|
||||
export async function fetchCurrentUser(): Promise<CurrentUser> {
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { NButton, NModal } from "naive-ui";
|
||||
import { fetchCurrentUser } from "@/api/auth";
|
||||
import { getApiKey } from "@/api/client";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
const show = ref(false);
|
||||
const loading = ref(false);
|
||||
const checkedToken = ref("");
|
||||
const promptedUserId = ref<number | null>(null);
|
||||
|
||||
function dismissalKey(userId: number): string {
|
||||
return `hermes_default_credentials_prompt_dismissed_${userId}`;
|
||||
}
|
||||
|
||||
async function checkDefaultCredentials() {
|
||||
if (route.name === "login") {
|
||||
show.value = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const token = getApiKey();
|
||||
if (!token || token === checkedToken.value) return;
|
||||
checkedToken.value = token;
|
||||
|
||||
loading.value = true;
|
||||
try {
|
||||
const user = await fetchCurrentUser();
|
||||
promptedUserId.value = user.id;
|
||||
const dismissed = sessionStorage.getItem(dismissalKey(user.id)) === "1";
|
||||
show.value = !!user.requiresCredentialChange && !dismissed;
|
||||
} catch {
|
||||
show.value = false;
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function remindLater() {
|
||||
if (promptedUserId.value != null) {
|
||||
sessionStorage.setItem(dismissalKey(promptedUserId.value), "1");
|
||||
}
|
||||
show.value = false;
|
||||
}
|
||||
|
||||
function goToAccountSettings() {
|
||||
show.value = false;
|
||||
router.push({ name: "hermes.settings", query: { tab: "account" } });
|
||||
}
|
||||
|
||||
watch(() => route.fullPath, () => {
|
||||
void checkDefaultCredentials();
|
||||
}, { immediate: true });
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NModal
|
||||
v-model:show="show"
|
||||
preset="dialog"
|
||||
:title="t('login.defaultCredentialTitle')"
|
||||
:mask-closable="false"
|
||||
>
|
||||
<p class="credential-warning-text">
|
||||
{{ t("login.defaultCredentialMessage") }}
|
||||
</p>
|
||||
<template #action>
|
||||
<NButton :disabled="loading" @click="remindLater">
|
||||
{{ t("login.defaultCredentialLater") }}
|
||||
</NButton>
|
||||
<NButton type="primary" :loading="loading" @click="goToAccountSettings">
|
||||
{{ t("login.defaultCredentialAction") }}
|
||||
</NButton>
|
||||
</template>
|
||||
</NModal>
|
||||
</template>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.credential-warning-text {
|
||||
margin: 0;
|
||||
line-height: 1.6;
|
||||
}
|
||||
</style>
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: 'Token',
|
||||
usernamePlaceholder: 'Benutzername',
|
||||
passwordPlaceholder: 'Passwort',
|
||||
defaultCredentialsHint: 'Standard-Benutzername: admin. Standard-Passwort: 123456.',
|
||||
credentialsRequired: 'Bitte Benutzername und Passwort eingeben',
|
||||
invalidCredentials: 'Ungultiger Benutzername oder Passwort',
|
||||
tooManyAttempts: 'Zu viele fehlgeschlagene Versuche, bitte versuchen Sie es spater erneut',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: 'Passwort-Login ist fur Benutzerkonten erforderlich.',
|
||||
passwordLoginNotConfigured: 'Passwort-Login ist nicht konfiguriert',
|
||||
passwordLoginConfigured: 'Aktuelles Konto: {username}',
|
||||
defaultCredentialTitle: 'Standardkonto und Passwort andern',
|
||||
defaultCredentialMessage: 'Das aktuelle Konto verwendet noch den Standard-Benutzernamen oder das Standard-Passwort. Um unbefugten Zugriff zu vermeiden, andern Sie Benutzername und Passwort des aktuellen Kontos so bald wie moglich.',
|
||||
defaultCredentialAction: 'Jetzt andern',
|
||||
defaultCredentialLater: 'Spater erinnern',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: 'Token',
|
||||
usernamePlaceholder: 'Username',
|
||||
passwordPlaceholder: 'Password',
|
||||
defaultCredentialsHint: 'Default username: admin. Default password: 123456.',
|
||||
credentialsRequired: 'Please enter username and password',
|
||||
invalidCredentials: 'Invalid username or password',
|
||||
tooManyAttempts: 'Too many failed attempts, please try again later',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: 'Password login is required for user accounts.',
|
||||
passwordLoginNotConfigured: 'Password login is not configured',
|
||||
passwordLoginConfigured: 'Current account: {username}',
|
||||
defaultCredentialTitle: 'Change the default account credentials',
|
||||
defaultCredentialMessage: 'This account is still using the default username or password. To prevent unauthorized access, update the username and password as soon as possible.',
|
||||
defaultCredentialAction: 'Update now',
|
||||
defaultCredentialLater: 'Remind me later',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: 'Token',
|
||||
usernamePlaceholder: 'Nombre de usuario',
|
||||
passwordPlaceholder: 'Contrasena',
|
||||
defaultCredentialsHint: 'Nombre de usuario predeterminado: admin. Contrasena predeterminada: 123456.',
|
||||
credentialsRequired: 'Por favor, introduzca nombre de usuario y contrasena',
|
||||
invalidCredentials: 'Nombre de usuario o contrasena incorrectos',
|
||||
tooManyAttempts: 'Demasiados intentos fallidos, por favor intente mas tarde',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: 'El login con contrasena es obligatorio para las cuentas de usuario.',
|
||||
passwordLoginNotConfigured: 'Login con contrasena no configurado',
|
||||
passwordLoginConfigured: 'Cuenta actual: {username}',
|
||||
defaultCredentialTitle: 'Cambia la cuenta y contrasena predeterminadas',
|
||||
defaultCredentialMessage: 'La cuenta actual aun usa el nombre de usuario o la contrasena predeterminados. Para evitar accesos no autorizados, cambia cuanto antes el nombre de usuario y la contrasena de la cuenta actual.',
|
||||
defaultCredentialAction: 'Cambiar ahora',
|
||||
defaultCredentialLater: 'Recordar mas tarde',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: 'Jeton',
|
||||
usernamePlaceholder: 'Nom d\'utilisateur',
|
||||
passwordPlaceholder: 'Mot de passe',
|
||||
defaultCredentialsHint: 'Nom d utilisateur par defaut : admin. Mot de passe par defaut : 123456.',
|
||||
credentialsRequired: 'Veuillez entrer le nom d\'utilisateur et le mot de passe',
|
||||
invalidCredentials: 'Nom d\'utilisateur ou mot de passe incorrect',
|
||||
tooManyAttempts: 'Trop de tentatives echouees, veuillez reessayer plus tard',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: 'Le login par mot de passe est requis pour les comptes utilisateur.',
|
||||
passwordLoginNotConfigured: 'Login par mot de passe non configure',
|
||||
passwordLoginConfigured: 'Compte actuel : {username}',
|
||||
defaultCredentialTitle: 'Modifiez le compte et le mot de passe par defaut',
|
||||
defaultCredentialMessage: 'Le compte connecte utilise encore le nom d utilisateur ou le mot de passe par defaut. Pour eviter tout acces non autorise, modifiez rapidement le nom d utilisateur et le mot de passe du compte actuel.',
|
||||
defaultCredentialAction: 'Modifier maintenant',
|
||||
defaultCredentialLater: 'Me le rappeler plus tard',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: 'トークン',
|
||||
usernamePlaceholder: 'ユーザー名',
|
||||
passwordPlaceholder: 'パスワード',
|
||||
defaultCredentialsHint: '既定のユーザー名:admin、既定のパスワード:123456',
|
||||
credentialsRequired: 'ユーザー名とパスワードを入力してください',
|
||||
invalidCredentials: 'ユーザー名またはパスワードが正しくありません',
|
||||
tooManyAttempts: 'ログイン試行回数が多すぎます。しばらくしてからお試しください',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: 'ユーザーアカウントにはパスワードログインが必要です。',
|
||||
passwordLoginNotConfigured: 'パスワードログイン未設定',
|
||||
passwordLoginConfigured: '現在のアカウント:{username}',
|
||||
defaultCredentialTitle: '既定のアカウント情報を変更してください',
|
||||
defaultCredentialMessage: '現在のログインアカウントは、既定のユーザー名または既定のパスワードをまだ使用しています。不正アクセスを防ぐため、できるだけ早く現在のアカウントでユーザー名とパスワードを変更してください。',
|
||||
defaultCredentialAction: '変更する',
|
||||
defaultCredentialLater: '後で通知',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: '토큰',
|
||||
usernamePlaceholder: '사용자 이름',
|
||||
passwordPlaceholder: '비밀번호',
|
||||
defaultCredentialsHint: '기본 로그인 이름: admin, 기본 비밀번호: 123456',
|
||||
credentialsRequired: '사용자 이름과 비밀번호를 입력해 주세요',
|
||||
invalidCredentials: '사용자 이름 또는 비밀번호가 올바르지 않습니다',
|
||||
tooManyAttempts: '로그인 시도 횟수가 너무 많습니다. 잠시 후 다시 시도해 주세요',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: '사용자 계정에는 비밀번호 로그인이 필요합니다.',
|
||||
passwordLoginNotConfigured: '비밀번호 로그인 미설정',
|
||||
passwordLoginConfigured: '현재 계정: {username}',
|
||||
defaultCredentialTitle: '기본 계정과 비밀번호를 변경하세요',
|
||||
defaultCredentialMessage: '현재 로그인 계정이 아직 기본 사용자 이름 또는 기본 비밀번호를 사용하고 있습니다. 무단 접근을 방지하려면 현재 계정에서 사용자 이름과 비밀번호를 가능한 한 빨리 변경하세요.',
|
||||
defaultCredentialAction: '변경하기',
|
||||
defaultCredentialLater: '나중에 알림',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: 'Token',
|
||||
usernamePlaceholder: 'Nome de usuario',
|
||||
passwordPlaceholder: 'Senha',
|
||||
defaultCredentialsHint: 'Nome de usuario padrao: admin. Senha padrao: 123456.',
|
||||
credentialsRequired: 'Por favor, insira nome de usuario e senha',
|
||||
invalidCredentials: 'Nome de usuario ou senha incorretos',
|
||||
tooManyAttempts: 'Muitas tentativas falhadas, por favor tente novamente mais tarde',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: 'Login por senha e obrigatorio para contas de usuario.',
|
||||
passwordLoginNotConfigured: 'Login por senha nao configurado',
|
||||
passwordLoginConfigured: 'Conta atual: {username}',
|
||||
defaultCredentialTitle: 'Altere a conta e senha padrao',
|
||||
defaultCredentialMessage: 'A conta atual ainda usa o nome de usuario ou a senha padrao. Para evitar acesso nao autorizado, altere o nome de usuario e a senha da conta atual o quanto antes.',
|
||||
defaultCredentialAction: 'Alterar agora',
|
||||
defaultCredentialLater: 'Lembrar depois',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: '權杖登入',
|
||||
usernamePlaceholder: '使用者名稱',
|
||||
passwordPlaceholder: '密碼',
|
||||
defaultCredentialsHint: '預設登入名:admin,預設密碼:123456',
|
||||
credentialsRequired: '請輸入使用者名稱和密碼',
|
||||
invalidCredentials: '使用者名稱或密碼錯誤',
|
||||
tooManyAttempts: '登入失敗次數過多,請稍後再試',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: '使用者帳號必須保留密碼登入。',
|
||||
passwordLoginNotConfigured: '密碼登入未設定',
|
||||
passwordLoginConfigured: '目前帳號:{username}',
|
||||
defaultCredentialTitle: '請修改預設帳號和密碼',
|
||||
defaultCredentialMessage: '目前登入帳號仍在使用預設使用者名稱或預設密碼。為避免未授權存取,請盡快進入目前帳號修改使用者名稱和密碼。',
|
||||
defaultCredentialAction: '去修改',
|
||||
defaultCredentialLater: '稍後提醒',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -12,6 +12,7 @@ export default {
|
||||
tokenLogin: '令牌登录',
|
||||
usernamePlaceholder: '用户名',
|
||||
passwordPlaceholder: '密码',
|
||||
defaultCredentialsHint: '默认登录名:admin,默认密码:123456',
|
||||
credentialsRequired: '请输入用户名和密码',
|
||||
invalidCredentials: '用户名或密码错误',
|
||||
tooManyAttempts: '登录失败次数过多,请稍后重试',
|
||||
@@ -37,6 +38,10 @@ export default {
|
||||
removeConfirm: '用户账号必须保留密码登录。',
|
||||
passwordLoginNotConfigured: '密码登录未配置',
|
||||
passwordLoginConfigured: '当前账户:{username}',
|
||||
defaultCredentialTitle: '请修改默认账户和密码',
|
||||
defaultCredentialMessage: '当前登录账户仍在使用默认用户名或默认密码。为了避免未授权访问,请尽快进入当前账户修改用户名和密码。',
|
||||
defaultCredentialAction: '去修改',
|
||||
defaultCredentialLater: '稍后提醒',
|
||||
},
|
||||
|
||||
users: {
|
||||
|
||||
@@ -63,6 +63,7 @@ async function handlePasswordLogin() {
|
||||
</div>
|
||||
<h1 class="login-title">{{ t("login.title") }}</h1>
|
||||
<p class="login-desc">{{ t("login.description") }}</p>
|
||||
<p class="login-default-hint">{{ t("login.defaultCredentialsHint") }}</p>
|
||||
|
||||
<form class="login-form" @submit.prevent="handleLogin">
|
||||
<input
|
||||
@@ -128,10 +129,17 @@ async function handlePasswordLogin() {
|
||||
.login-desc {
|
||||
font-size: 14px;
|
||||
color: $text-muted;
|
||||
margin: 0 0 32px;
|
||||
margin: 0 0 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.login-default-hint {
|
||||
margin: 0 0 28px;
|
||||
font-family: $font-code;
|
||||
font-size: 13px;
|
||||
color: $text-secondary;
|
||||
}
|
||||
|
||||
.login-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from "vue";
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
import {
|
||||
NTabs,
|
||||
NTabPane,
|
||||
@@ -22,6 +23,41 @@ import { isStoredSuperAdmin } from "@/api/client";
|
||||
const settingsStore = useSettingsStore();
|
||||
const { t } = useI18n();
|
||||
const canManageUsers = isStoredSuperAdmin();
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
const activeTab = ref("account");
|
||||
|
||||
const validTabs = computed(() => new Set([
|
||||
"account",
|
||||
...(canManageUsers ? ["users"] : []),
|
||||
"display",
|
||||
"agent",
|
||||
"memory",
|
||||
"compression",
|
||||
"session",
|
||||
"privacy",
|
||||
"models",
|
||||
"voice",
|
||||
]));
|
||||
|
||||
function normalizeTab(value: unknown): string {
|
||||
const tab = typeof value === "string" ? value : "";
|
||||
return validTabs.value.has(tab) ? tab : "account";
|
||||
}
|
||||
|
||||
function handleTabUpdate(tab: string) {
|
||||
activeTab.value = normalizeTab(tab);
|
||||
router.replace({
|
||||
query: {
|
||||
...route.query,
|
||||
tab: activeTab.value === "account" ? undefined : activeTab.value,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
watch(() => route.query.tab, (tab) => {
|
||||
activeTab.value = normalizeTab(tab);
|
||||
}, { immediate: true });
|
||||
|
||||
onMounted(() => {
|
||||
settingsStore.fetchSettings();
|
||||
@@ -40,7 +76,7 @@ onMounted(() => {
|
||||
size="large"
|
||||
:description="t('common.loading')"
|
||||
>
|
||||
<NTabs type="line" animated>
|
||||
<NTabs v-model:value="activeTab" type="line" animated @update:value="handleTabUpdate">
|
||||
<NTabPane name="account" :tab="t('settings.tabs.account')">
|
||||
<AccountSettings />
|
||||
</NTabPane>
|
||||
|
||||
Reference in New Issue
Block a user