feat(admin-web/login): replace default test credentials with remembered login

Why: showing the seed admin account on the login screen is unsafe for production deployments.
How to apply: store identifier/password in localStorage when "记住密码" is checked, restore them on mount, and drop the demo credentials panel.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-30 18:15:08 +08:00
parent 1e46bfa1f4
commit 9525c1144d
3 changed files with 48 additions and 46 deletions
+2 -2
View File
@@ -95,8 +95,8 @@ const enUS = {
email: "Email",
loginIdentifier: "Phone / Email",
password: "Password",
defaultTestAccount: "Default account",
defaultTestPassword: "Default password",
rememberPassword: "Remember password",
forgotPassword: "Forgot password",
},
membershipBlocked: {
eyebrow: "Access Paused",
+2 -2
View File
@@ -96,8 +96,8 @@ const zhCN = {
email: "邮箱",
loginIdentifier: "手机号 / 邮箱",
password: "密码",
defaultTestAccount: "默认测试账号",
defaultTestPassword: "默认测试密码",
rememberPassword: "记住密码",
forgotPassword: "忘记密码",
},
membershipBlocked: {
eyebrow: "访问已暂停",
+44 -42
View File
@@ -40,7 +40,7 @@
<label for="login-password">{{ t('auth.password') }}</label>
<div class="password-wrap">
<input id="login-password" :type="showPassword ? 'text' : 'password'"
placeholder="Admin@123" v-model="formState.password" required />
:placeholder="t('auth.password')" v-model="formState.password" required />
<button type="button" class="eye-btn" @click="showPassword = !showPassword">
<EyeInvisibleOutlined v-if="showPassword" />
<EyeOutlined v-else />
@@ -48,33 +48,22 @@
</div>
</div>
<div class="options">
<label class="remember"><input type="checkbox" v-model="remember" /> Remember for 30 days</label>
<a href="#" class="forgot">Forgot password?</a>
<label class="remember"><input type="checkbox" v-model="remember" /> {{ t('auth.rememberPassword') }}</label>
<a href="#" class="forgot">{{ t('auth.forgotPassword') }}</a>
</div>
<button type="submit" class="btn-primary" :disabled="submitting"
:style="{ background: primaryColor }">
{{ submitting ? t('auth.loginAndEnter') + '...' : t('auth.loginAndEnter') }}
</button>
</form>
<div class="login-credentials">
<div class="login-credentials__row">
<span>{{ t("auth.defaultTestAccount") }}</span>
<strong>admin@geo.local</strong>
</div>
<div class="login-credentials__row">
<span>{{ t("auth.defaultTestPassword") }}</span>
<strong>Admin@123</strong>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from "vue";
import { onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n";
import { message } from "ant-design-vue";
@@ -84,6 +73,8 @@ import { formatError } from "@/lib/errors";
import { useAuthStore } from "@/stores/auth";
import AnimatedCharacters from "@/components/login-animation/AnimatedCharacters.vue";
const REMEMBER_STORAGE_KEY = "geo:admin:login:remember";
const authStore = useAuthStore();
const router = useRouter();
const route = useRoute();
@@ -97,10 +88,44 @@ const isTyping = ref(false);
const remember = ref(false);
const formState = reactive({
identifier: "admin@geo.local",
password: "Admin@123",
identifier: "",
password: "",
});
onMounted(() => {
if (typeof window === "undefined") {
return;
}
try {
const raw = window.localStorage.getItem(REMEMBER_STORAGE_KEY);
if (!raw) {
return;
}
const parsed = JSON.parse(raw) as { identifier?: string; password?: string };
if (parsed && typeof parsed.identifier === "string" && typeof parsed.password === "string") {
formState.identifier = parsed.identifier;
formState.password = parsed.password;
remember.value = true;
}
} catch {
window.localStorage.removeItem(REMEMBER_STORAGE_KEY);
}
});
function persistRememberedCredentials(): void {
if (typeof window === "undefined") {
return;
}
if (remember.value) {
window.localStorage.setItem(
REMEMBER_STORAGE_KEY,
JSON.stringify({ identifier: formState.identifier, password: formState.password }),
);
} else {
window.localStorage.removeItem(REMEMBER_STORAGE_KEY);
}
}
async function handleSubmit(): Promise<void> {
if (submitting.value) {
return;
@@ -110,6 +135,7 @@ async function handleSubmit(): Promise<void> {
try {
await authStore.login(formState);
persistRememberedCredentials();
const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/workspace";
await router.replace(redirect);
} catch (error) {
@@ -186,30 +212,6 @@ async function handleSubmit(): Promise<void> {
.btn-primary:hover { opacity: 0.9; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
.login-credentials {
margin-top: 24px;
padding: 16px;
background: #f8fafc;
border: 1px solid #e6edf5;
border-radius: 12px;
}
.login-credentials__row {
display: flex;
justify-content: space-between;
gap: 12px;
}
.login-credentials__row + .login-credentials__row {
margin-top: 8px;
}
.login-credentials__row span {
color: var(--muted, #71717a);
font-size: 13px;
}
.login-credentials__row strong {
color: #18181b;
font-size: 13px;
}
@media (max-width: 1023px) {
.page { grid-template-columns: 1fr; }
.left { display: none; }