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", email: "Email",
loginIdentifier: "Phone / Email", loginIdentifier: "Phone / Email",
password: "Password", password: "Password",
defaultTestAccount: "Default account", rememberPassword: "Remember password",
defaultTestPassword: "Default password", forgotPassword: "Forgot password",
}, },
membershipBlocked: { membershipBlocked: {
eyebrow: "Access Paused", eyebrow: "Access Paused",
+2 -2
View File
@@ -96,8 +96,8 @@ const zhCN = {
email: "邮箱", email: "邮箱",
loginIdentifier: "手机号 / 邮箱", loginIdentifier: "手机号 / 邮箱",
password: "密码", password: "密码",
defaultTestAccount: "默认测试账号", rememberPassword: "记住密码",
defaultTestPassword: "默认测试密码", forgotPassword: "忘记密码",
}, },
membershipBlocked: { membershipBlocked: {
eyebrow: "访问已暂停", eyebrow: "访问已暂停",
+44 -42
View File
@@ -40,7 +40,7 @@
<label for="login-password">{{ t('auth.password') }}</label> <label for="login-password">{{ t('auth.password') }}</label>
<div class="password-wrap"> <div class="password-wrap">
<input id="login-password" :type="showPassword ? 'text' : 'password'" <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"> <button type="button" class="eye-btn" @click="showPassword = !showPassword">
<EyeInvisibleOutlined v-if="showPassword" /> <EyeInvisibleOutlined v-if="showPassword" />
<EyeOutlined v-else /> <EyeOutlined v-else />
@@ -48,33 +48,22 @@
</div> </div>
</div> </div>
<div class="options"> <div class="options">
<label class="remember"><input type="checkbox" v-model="remember" /> Remember for 30 days</label> <label class="remember"><input type="checkbox" v-model="remember" /> {{ t('auth.rememberPassword') }}</label>
<a href="#" class="forgot">Forgot password?</a> <a href="#" class="forgot">{{ t('auth.forgotPassword') }}</a>
</div> </div>
<button type="submit" class="btn-primary" :disabled="submitting" <button type="submit" class="btn-primary" :disabled="submitting"
:style="{ background: primaryColor }"> :style="{ background: primaryColor }">
{{ submitting ? t('auth.loginAndEnter') + '...' : t('auth.loginAndEnter') }} {{ submitting ? t('auth.loginAndEnter') + '...' : t('auth.loginAndEnter') }}
</button> </button>
</form> </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> </div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { reactive, ref } from "vue"; import { onMounted, reactive, ref } from "vue";
import { useRoute, useRouter } from "vue-router"; import { useRoute, useRouter } from "vue-router";
import { useI18n } from "vue-i18n"; import { useI18n } from "vue-i18n";
import { message } from "ant-design-vue"; import { message } from "ant-design-vue";
@@ -84,6 +73,8 @@ import { formatError } from "@/lib/errors";
import { useAuthStore } from "@/stores/auth"; import { useAuthStore } from "@/stores/auth";
import AnimatedCharacters from "@/components/login-animation/AnimatedCharacters.vue"; import AnimatedCharacters from "@/components/login-animation/AnimatedCharacters.vue";
const REMEMBER_STORAGE_KEY = "geo:admin:login:remember";
const authStore = useAuthStore(); const authStore = useAuthStore();
const router = useRouter(); const router = useRouter();
const route = useRoute(); const route = useRoute();
@@ -97,10 +88,44 @@ const isTyping = ref(false);
const remember = ref(false); const remember = ref(false);
const formState = reactive({ const formState = reactive({
identifier: "admin@geo.local", identifier: "",
password: "Admin@123", 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> { async function handleSubmit(): Promise<void> {
if (submitting.value) { if (submitting.value) {
return; return;
@@ -110,6 +135,7 @@ async function handleSubmit(): Promise<void> {
try { try {
await authStore.login(formState); await authStore.login(formState);
persistRememberedCredentials();
const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/workspace"; const redirect = typeof route.query.redirect === "string" ? route.query.redirect : "/workspace";
await router.replace(redirect); await router.replace(redirect);
} catch (error) { } catch (error) {
@@ -186,30 +212,6 @@ async function handleSubmit(): Promise<void> {
.btn-primary:hover { opacity: 0.9; } .btn-primary:hover { opacity: 0.9; }
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; } .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) { @media (max-width: 1023px) {
.page { grid-template-columns: 1fr; } .page { grid-template-columns: 1fr; }
.left { display: none; } .left { display: none; }