Add default credential reset safeguards

This commit is contained in:
ekko
2026-05-24 09:49:21 +08:00
committed by ekko
parent 9708a6a521
commit f8a1b2f6ae
22 changed files with 565 additions and 7 deletions
+2
View File
@@ -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>
+1
View File
@@ -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>
+5
View File
@@ -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: {
+5
View File
@@ -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: {
+5
View File
@@ -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: {
+5
View File
@@ -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: {
+5
View File
@@ -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: {
+5
View File
@@ -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: {
+5
View File
@@ -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: {
+5
View File
@@ -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: {
+9 -1
View File
@@ -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>