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:
@@ -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",
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ const zhCN = {
|
|||||||
email: "邮箱",
|
email: "邮箱",
|
||||||
loginIdentifier: "手机号 / 邮箱",
|
loginIdentifier: "手机号 / 邮箱",
|
||||||
password: "密码",
|
password: "密码",
|
||||||
defaultTestAccount: "默认测试账号",
|
rememberPassword: "记住密码",
|
||||||
defaultTestPassword: "默认测试密码",
|
forgotPassword: "忘记密码",
|
||||||
},
|
},
|
||||||
membershipBlocked: {
|
membershipBlocked: {
|
||||||
eyebrow: "访问已暂停",
|
eyebrow: "访问已暂停",
|
||||||
|
|||||||
@@ -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; }
|
||||||
|
|||||||
Reference in New Issue
Block a user