mirror of https://github.com/halo-dev/halo
refactor: remove login-related pages from UI project (#6712)
#### What type of PR is this? /area ui /kind improvement /milestone 2.20.x #### What this PR does / why we need it: 移除 UI 项目中和登录、注册相关的页面和代码,后续将由后端统一提供:https://github.com/halo-dev/halo/pull/6488 相关 issue:https://github.com/halo-dev/halo/issues/5214 #### Which issue(s) this PR fixes: Fixes https://github.com/halo-dev/halo/issues/5214 #### Does this PR introduce a user-facing change? ```release-note None ```pull/6703/head^2
parent
a4c906706f
commit
d5233963fb
|
@ -1,6 +1,5 @@
|
|||
<script lang="ts" setup>
|
||||
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
||||
import LoginModal from "@/components/login/LoginModal.vue";
|
||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
|
@ -303,7 +302,6 @@ onMounted(() => {
|
|||
v-if="globalSearchVisible"
|
||||
@close="globalSearchVisible = false"
|
||||
/>
|
||||
<LoginModal />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -1,11 +1,10 @@
|
|||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import type { Router } from "vue-router";
|
||||
|
||||
const whiteList = ["Setup", "Login", "Binding", "ResetPassword", "Redirect"];
|
||||
const whiteList = ["Setup"];
|
||||
|
||||
export function setupAuthCheckGuard(router: Router) {
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach((to, _, next) => {
|
||||
const userStore = useUserStore();
|
||||
|
||||
if (userStore.isAnonymous) {
|
||||
|
@ -13,67 +12,9 @@ export function setupAuthCheckGuard(router: Router) {
|
|||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
next({
|
||||
name: "Login",
|
||||
query: {
|
||||
redirect_uri: encodeURIComponent(window.location.href),
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
if (to.name === "Login") {
|
||||
if (to.query.redirect_uri) {
|
||||
next({
|
||||
name: "Redirect",
|
||||
query: {
|
||||
redirect_uri: to.query.redirect_uri,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const roleHasRedirectOnLogin = userStore.currentRoles?.find(
|
||||
(role) =>
|
||||
role.metadata.annotations?.[rbacAnnotations.REDIRECT_ON_LOGIN]
|
||||
);
|
||||
|
||||
if (roleHasRedirectOnLogin) {
|
||||
window.location.href =
|
||||
roleHasRedirectOnLogin.metadata.annotations?.[
|
||||
rbacAnnotations.REDIRECT_ON_LOGIN
|
||||
] || "/uc";
|
||||
return;
|
||||
}
|
||||
|
||||
next({
|
||||
name: "Dashboard",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (to.name && whiteList.includes(to.name as string)) {
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check allow access console
|
||||
const { currentRoles } = userStore;
|
||||
|
||||
const hasDisallowAccessConsoleRole = currentRoles?.some((role) => {
|
||||
return (
|
||||
role.metadata.annotations?.[
|
||||
rbacAnnotations.DISALLOW_ACCESS_CONSOLE
|
||||
] === "true"
|
||||
);
|
||||
});
|
||||
|
||||
if (hasDisallowAccessConsoleRole) {
|
||||
window.location.href = "/uc";
|
||||
return;
|
||||
}
|
||||
|
||||
next();
|
||||
window.location.href = `/login?redirect_uri=${encodeURIComponent(
|
||||
window.location.href
|
||||
)}`;
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { useUserStore } from "@/stores/user";
|
|||
import type { Router } from "vue-router";
|
||||
|
||||
export function setupCheckStatesGuard(router: Router) {
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
router.beforeEach(async (to, _, next) => {
|
||||
const userStore = useUserStore();
|
||||
const { globalInfo } = useGlobalInfoStore();
|
||||
const { userInitialized, dataInitialized } = globalInfo || {};
|
||||
|
|
|
@ -3,7 +3,7 @@ import { hasPermission } from "@/utils/permission";
|
|||
import type { Router } from "vue-router";
|
||||
|
||||
export function setupPermissionGuard(router: Router) {
|
||||
router.beforeEach((to, from, next) => {
|
||||
router.beforeEach((to, _, next) => {
|
||||
const roleStore = useRoleStore();
|
||||
const { uiPermissions } = roleStore.permissions;
|
||||
const { meta } = to;
|
||||
|
|
|
@ -1,11 +1,6 @@
|
|||
import GatewayLayout from "@/layouts/GatewayLayout.vue";
|
||||
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
||||
import NotFound from "@/views/exceptions/NotFound.vue";
|
||||
import BasicLayout from "@console/layouts/BasicLayout.vue";
|
||||
import Binding from "@console/views/system/Binding.vue";
|
||||
import Login from "@console/views/system/Login.vue";
|
||||
import Redirect from "@console/views/system/Redirect.vue";
|
||||
import ResetPassword from "@console/views/system/ResetPassword.vue";
|
||||
import Setup from "@console/views/system/Setup.vue";
|
||||
import SetupInitialData from "@console/views/system/SetupInitialData.vue";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
@ -27,34 +22,6 @@ export const routes: Array<RouteRecordRaw> = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/login",
|
||||
component: GatewayLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "Login",
|
||||
component: Login,
|
||||
meta: {
|
||||
title: "core.login.title",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/binding/:provider",
|
||||
component: GatewayLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "Binding",
|
||||
component: Binding,
|
||||
meta: {
|
||||
title: "core.binding.title",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/setup",
|
||||
component: Setup,
|
||||
|
@ -71,25 +38,6 @@ export const routes: Array<RouteRecordRaw> = [
|
|||
title: "core.setup.title",
|
||||
},
|
||||
},
|
||||
{
|
||||
path: "/redirect",
|
||||
name: "Redirect",
|
||||
component: Redirect,
|
||||
},
|
||||
{
|
||||
path: "/reset-password",
|
||||
component: GatewayLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "ResetPassword",
|
||||
component: ResetPassword,
|
||||
meta: {
|
||||
title: "core.reset_password.title",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
|
|
@ -1,87 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import LoginForm from "@/components/login/LoginForm.vue";
|
||||
import SignupForm from "@/components/signup/SignupForm.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { useGlobalInfoFetch } from "@console/composables/use-global-info";
|
||||
import router from "@console/router";
|
||||
import { Toast } from "@halo-dev/components";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { computed, onBeforeMount, onMounted } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useRoute } from "vue-router";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const route = useRoute();
|
||||
const { t } = useI18n();
|
||||
|
||||
onBeforeMount(() => {
|
||||
if (!userStore.isAnonymous) {
|
||||
router.push({ name: "Dashboard" });
|
||||
}
|
||||
});
|
||||
|
||||
const { globalInfo } = useGlobalInfoFetch();
|
||||
|
||||
onMounted(() => {
|
||||
Toast.warning(t("core.binding.common.toast.mounted"));
|
||||
});
|
||||
|
||||
function handleBinding() {
|
||||
const authProvider = globalInfo.value?.socialAuthProviders.find(
|
||||
(p) => p.name === route.params.provider
|
||||
);
|
||||
|
||||
if (!authProvider?.bindingUrl) {
|
||||
Toast.error(t("core.binding.operations.bind.toast_failed"));
|
||||
return;
|
||||
}
|
||||
|
||||
window.location.href = authProvider?.bindingUrl;
|
||||
|
||||
Toast.success(t("core.binding.operations.bind.toast_success"));
|
||||
}
|
||||
|
||||
const type = useRouteQuery<string>("type", "");
|
||||
|
||||
function handleChangeType() {
|
||||
type.value = type.value === "signup" ? "" : "signup";
|
||||
}
|
||||
|
||||
const isLoginType = computed(() => type.value !== "signup");
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex w-72 flex-col">
|
||||
<SignupForm
|
||||
v-if="type === 'signup'"
|
||||
button-text="core.binding.operations.signup_and_bind.button"
|
||||
@succeed="handleBinding"
|
||||
/>
|
||||
<LoginForm
|
||||
v-else
|
||||
button-text="core.binding.operations.login_and_bind.button"
|
||||
@succeed="handleBinding"
|
||||
/>
|
||||
<div
|
||||
v-if="globalInfo?.allowRegistration"
|
||||
class="flex justify-center gap-1 pt-3.5 text-xs"
|
||||
>
|
||||
<span class="text-slate-500">
|
||||
{{
|
||||
isLoginType
|
||||
? $t("core.login.operations.signup.label")
|
||||
: $t("core.login.operations.return_login.label")
|
||||
}}
|
||||
</span>
|
||||
<span
|
||||
class="cursor-pointer text-secondary hover:text-gray-600"
|
||||
@click="handleChangeType"
|
||||
>
|
||||
{{
|
||||
isLoginType
|
||||
? $t("core.login.operations.signup.button")
|
||||
: $t("core.login.operations.return_login.button")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
|
@ -1,85 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import LocaleChange from "@/components/common/LocaleChange.vue";
|
||||
import LoginForm from "@/components/login/LoginForm.vue";
|
||||
import SocialAuthProviders from "@/components/login/SocialAuthProviders.vue";
|
||||
import SignupForm from "@/components/signup/SignupForm.vue";
|
||||
import { useAppTitle } from "@/composables/use-title";
|
||||
import { useGlobalInfoFetch } from "@console/composables/use-global-info";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { computed } from "vue";
|
||||
import MdiKeyboardBackspace from "~icons/mdi/keyboard-backspace";
|
||||
|
||||
const { globalInfo } = useGlobalInfoFetch();
|
||||
|
||||
const SIGNUP_TYPE = "signup";
|
||||
|
||||
function onLoginSucceed() {
|
||||
window.location.reload();
|
||||
}
|
||||
|
||||
function onSignupSucceed() {
|
||||
window.location.href = "/uc";
|
||||
}
|
||||
|
||||
const type = useRouteQuery<string>("type", "");
|
||||
|
||||
function handleChangeType() {
|
||||
type.value = type.value === SIGNUP_TYPE ? "" : SIGNUP_TYPE;
|
||||
}
|
||||
|
||||
const isLoginType = computed(() => type.value !== SIGNUP_TYPE);
|
||||
|
||||
useAppTitle(
|
||||
computed(
|
||||
() => `core.${type.value === SIGNUP_TYPE ? SIGNUP_TYPE : "login"}.title`
|
||||
)
|
||||
);
|
||||
</script>
|
||||
<template>
|
||||
<div class="flex w-72 flex-col">
|
||||
<SignupForm v-if="type === 'signup'" @succeed="onSignupSucceed" />
|
||||
<LoginForm v-else @succeed="onLoginSucceed" />
|
||||
<SocialAuthProviders />
|
||||
<div class="flex justify-center gap-2 pt-3.5 text-xs">
|
||||
<div v-if="globalInfo?.allowRegistration" class="space-x-0.5">
|
||||
<span class="text-slate-500">
|
||||
{{
|
||||
isLoginType
|
||||
? $t("core.login.operations.signup.label")
|
||||
: $t("core.login.operations.return_login.label")
|
||||
}},
|
||||
</span>
|
||||
<span
|
||||
class="cursor-pointer text-secondary hover:text-gray-600"
|
||||
@click="handleChangeType"
|
||||
>
|
||||
{{
|
||||
isLoginType
|
||||
? $t("core.login.operations.signup.button")
|
||||
: $t("core.login.operations.return_login.button")
|
||||
}}
|
||||
</span>
|
||||
</div>
|
||||
<RouterLink
|
||||
:to="{ name: 'ResetPassword' }"
|
||||
class="text-secondary hover:text-gray-600"
|
||||
>
|
||||
{{ $t("core.login.operations.reset_password.button") }}
|
||||
</RouterLink>
|
||||
</div>
|
||||
<div class="flex justify-center pt-3.5">
|
||||
<a
|
||||
class="inline-flex items-center gap-0.5 text-xs text-gray-600 hover:text-gray-900"
|
||||
href="/"
|
||||
>
|
||||
<MdiKeyboardBackspace class="!h-3.5 !w-3.5" />
|
||||
<span> {{ $t("core.login.operations.return_site") }} </span>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bottom-0 mb-10 mt-auto flex items-center justify-center gap-2.5 pt-3.5"
|
||||
>
|
||||
<LocaleChange />
|
||||
</div>
|
||||
</template>
|
|
@ -1,41 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { useRoute, useRouter } from "vue-router";
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
if (allowRedirect()) {
|
||||
window.location.href = decodeURIComponent(route.query.redirect_uri as string);
|
||||
} else {
|
||||
router.push({
|
||||
name: "Dashboard",
|
||||
});
|
||||
}
|
||||
|
||||
function allowRedirect() {
|
||||
const redirect_uri = decodeURIComponent(route.query.redirect_uri as string);
|
||||
|
||||
if (!redirect_uri || redirect_uri === window.location.href) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (redirect_uri.startsWith("/")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
redirect_uri.startsWith("https://") ||
|
||||
redirect_uri.startsWith("http://")
|
||||
) {
|
||||
const url = new URL(redirect_uri);
|
||||
if (url.origin === window.location.origin) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div id="loader"></div>
|
||||
</template>
|
|
@ -1,81 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { publicApiClient } from "@halo-dev/api-client";
|
||||
import { Toast, VButton } from "@halo-dev/components";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface ResetPasswordForm {
|
||||
email: string;
|
||||
username: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function onSubmit(data: ResetPasswordForm) {
|
||||
try {
|
||||
loading.value = true;
|
||||
await publicApiClient.user.sendPasswordResetEmail({
|
||||
passwordResetEmailRequest: {
|
||||
email: data.email,
|
||||
username: data.username,
|
||||
},
|
||||
});
|
||||
|
||||
Toast.success(t("core.reset_password.operations.send.toast_success"));
|
||||
} catch (error) {
|
||||
console.error("Failed to send password reset email", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const inputClasses = {
|
||||
outer: "!py-3 first:!pt-0 last:!pb-0",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-72 flex-col">
|
||||
<FormKit
|
||||
id="reset-password-form"
|
||||
name="reset-password-form"
|
||||
type="form"
|
||||
:classes="{
|
||||
form: '!divide-none',
|
||||
}"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
@submit="onSubmit"
|
||||
@keyup.enter="$formkit.submit('reset-password-form')"
|
||||
>
|
||||
<FormKit
|
||||
:classes="inputClasses"
|
||||
name="username"
|
||||
:placeholder="$t('core.reset_password.fields.username.label')"
|
||||
:validation-label="$t('core.reset_password.fields.username.label')"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
:classes="inputClasses"
|
||||
name="email"
|
||||
:placeholder="$t('core.reset_password.fields.email.label')"
|
||||
:validation-label="$t('core.reset_password.fields.email.label')"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<VButton
|
||||
class="mt-8"
|
||||
block
|
||||
:loading="loading"
|
||||
type="secondary"
|
||||
@click="$formkit.submit('reset-password-form')"
|
||||
>
|
||||
{{ $t("core.reset_password.operations.send.label") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</template>
|
|
@ -1,188 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { ERROR_MFA_REQUIRED_TYPE } from "@/constants/error-types";
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { randomUUID } from "@/utils/id";
|
||||
import { reset, submitForm } from "@formkit/core";
|
||||
import { consoleApiClient } from "@halo-dev/api-client";
|
||||
import { Toast, VButton } from "@halo-dev/components";
|
||||
import axios, { AxiosError } from "axios";
|
||||
import { JSEncrypt } from "jsencrypt";
|
||||
import qs from "qs";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import MfaForm from "./MfaForm.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
buttonText?: string;
|
||||
}>(),
|
||||
{
|
||||
buttonText: "core.login.button",
|
||||
}
|
||||
);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "succeed"): void;
|
||||
}>();
|
||||
|
||||
const userStore = useUserStore();
|
||||
|
||||
const _csrf = ref("");
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const handleGenerateToken = async () => {
|
||||
const token = randomUUID();
|
||||
_csrf.value = token;
|
||||
const expires = new Date();
|
||||
expires.setFullYear(expires.getFullYear() + 1);
|
||||
document.cookie = `XSRF-TOKEN=${token}; Path=/; SameSite=Lax; expires=${expires.toUTCString()}`;
|
||||
};
|
||||
|
||||
async function handleLogin(data: {
|
||||
username: string;
|
||||
password: string;
|
||||
rememberMe: boolean;
|
||||
}) {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const { data: publicKey } = await consoleApiClient.login.getPublicKey();
|
||||
|
||||
const encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(publicKey.base64Format as string);
|
||||
|
||||
await axios.post(
|
||||
`/login?remember-me=${data.rememberMe}`,
|
||||
qs.stringify({
|
||||
_csrf: _csrf.value,
|
||||
username: data.username,
|
||||
password: encrypt.encrypt(data.password),
|
||||
}),
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
await userStore.fetchCurrentUser();
|
||||
|
||||
emit("succeed");
|
||||
} catch (e: unknown) {
|
||||
console.error("Failed to login", e);
|
||||
|
||||
if (e instanceof AxiosError) {
|
||||
if (/Network Error/.test(e.message)) {
|
||||
Toast.error(t("core.common.toast.network_error"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.response?.status === 403) {
|
||||
Toast.warning(t("core.login.operations.submit.toast_csrf"), {
|
||||
duration: 5000,
|
||||
});
|
||||
await handleGenerateToken();
|
||||
return;
|
||||
}
|
||||
|
||||
const {
|
||||
title: errorTitle,
|
||||
detail: errorDetail,
|
||||
type: errorType,
|
||||
} = e.response?.data || {};
|
||||
|
||||
if (errorType === ERROR_MFA_REQUIRED_TYPE) {
|
||||
mfaRequired.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (errorTitle || errorDetail) {
|
||||
Toast.error(errorDetail || errorTitle);
|
||||
} else {
|
||||
Toast.error(t("core.common.toast.unknown_error"));
|
||||
}
|
||||
} else {
|
||||
Toast.error(t("core.common.toast.unknown_error"));
|
||||
}
|
||||
|
||||
reset("passwordInput");
|
||||
setFocus("passwordInput");
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
handleGenerateToken();
|
||||
});
|
||||
|
||||
const inputClasses = {
|
||||
outer: "!py-3 first:!pt-0 last:!pb-0",
|
||||
};
|
||||
|
||||
// mfa
|
||||
const mfaRequired = ref(false);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<template v-if="!mfaRequired">
|
||||
<FormKit
|
||||
id="login-form"
|
||||
name="login-form"
|
||||
:actions="false"
|
||||
type="form"
|
||||
:classes="{
|
||||
form: '!divide-none',
|
||||
}"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
@submit="handleLogin"
|
||||
@keyup.enter="submitForm('login-form')"
|
||||
>
|
||||
<FormKit
|
||||
:classes="inputClasses"
|
||||
name="username"
|
||||
:placeholder="$t('core.login.fields.username.placeholder')"
|
||||
:validation-label="$t('core.login.fields.username.placeholder')"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
validation="required"
|
||||
>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
id="passwordInput"
|
||||
:classes="inputClasses"
|
||||
name="password"
|
||||
:placeholder="$t('core.login.fields.password.placeholder')"
|
||||
:validation-label="$t('core.login.fields.password.placeholder')"
|
||||
type="password"
|
||||
validation="required"
|
||||
autocomplete="current-password"
|
||||
>
|
||||
</FormKit>
|
||||
|
||||
<FormKit
|
||||
type="checkbox"
|
||||
:label="$t('core.login.fields.remember_me.label')"
|
||||
name="rememberMe"
|
||||
:value="false"
|
||||
:classes="inputClasses"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<VButton
|
||||
class="mt-6"
|
||||
block
|
||||
:loading="loading"
|
||||
type="secondary"
|
||||
@click="submitForm('login-form')"
|
||||
>
|
||||
{{ $t(buttonText) }}
|
||||
</VButton>
|
||||
</template>
|
||||
<MfaForm v-else @succeed="$emit('succeed')" />
|
||||
</template>
|
|
@ -1,37 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import LoginForm from "@/components/login/LoginForm.vue";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { Toast, VModal } from "@halo-dev/components";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import SocialAuthProviders from "./SocialAuthProviders.vue";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const { t } = useI18n();
|
||||
|
||||
const modal = ref<InstanceType<typeof VModal> | null>(null);
|
||||
|
||||
const onLoginSucceed = () => {
|
||||
modal.value?.close();
|
||||
Toast.success(t("core.login.operations.submit.toast_success"));
|
||||
};
|
||||
|
||||
function onClose() {
|
||||
userStore.loginModalVisible = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VModal
|
||||
v-if="userStore.loginModalVisible"
|
||||
ref="modal"
|
||||
:mount-to-body="true"
|
||||
:width="400"
|
||||
:centered="true"
|
||||
:title="$t('core.login.modal.title')"
|
||||
@close="onClose"
|
||||
>
|
||||
<LoginForm v-if="userStore.loginModalVisible" @succeed="onLoginSucceed" />
|
||||
<SocialAuthProviders />
|
||||
</VModal>
|
||||
</template>
|
|
@ -1,93 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { setFocus } from "@/formkit/utils/focus";
|
||||
import { submitForm } from "@formkit/core";
|
||||
import { Toast, VButton } from "@halo-dev/components";
|
||||
import axios from "axios";
|
||||
import qs from "qs";
|
||||
import { onMounted, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "succeed"): void;
|
||||
}>();
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function onSubmit({ code }: { code: string }) {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
const _csrf = document.cookie
|
||||
.split("; ")
|
||||
.find((row) => row.startsWith("XSRF-TOKEN"))
|
||||
?.split("=")[1];
|
||||
|
||||
if (!_csrf) {
|
||||
Toast.warning("CSRF token not found");
|
||||
return;
|
||||
}
|
||||
await axios.post(
|
||||
`/login/2fa/totp`,
|
||||
qs.stringify({
|
||||
code,
|
||||
_csrf,
|
||||
}),
|
||||
{
|
||||
withCredentials: true,
|
||||
headers: {
|
||||
"Content-Type": "application/x-www-form-urlencoded",
|
||||
"X-Requested-With": "XMLHttpRequest",
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
emit("succeed");
|
||||
} catch (error) {
|
||||
Toast.error(t("core.common.toast.validation_failed"));
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
setFocus("code");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormKit
|
||||
id="mfa-form"
|
||||
name="mfa-form"
|
||||
type="form"
|
||||
:classes="{
|
||||
form: '!divide-none',
|
||||
}"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
@submit="onSubmit"
|
||||
@keyup.enter="submitForm('mfa-form')"
|
||||
>
|
||||
<FormKit
|
||||
id="code"
|
||||
:classes="{
|
||||
outer: '!py-0',
|
||||
}"
|
||||
name="code"
|
||||
:placeholder="$t('core.login.2fa.fields.code.placeholder')"
|
||||
:validation-label="$t('core.login.2fa.fields.code.label')"
|
||||
type="text"
|
||||
validation="required"
|
||||
>
|
||||
</FormKit>
|
||||
</FormKit>
|
||||
<VButton
|
||||
:loading="loading"
|
||||
class="mt-8"
|
||||
block
|
||||
type="secondary"
|
||||
@click="submitForm('mfa-form')"
|
||||
>
|
||||
{{ $t("core.common.buttons.verify") }}
|
||||
</VButton>
|
||||
</template>
|
|
@ -1,81 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import type { SocialAuthProvider } from "@/types";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import type { Ref } from "vue";
|
||||
import { inject, ref } from "vue";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
authProvider: SocialAuthProvider;
|
||||
}>(),
|
||||
{}
|
||||
);
|
||||
|
||||
const REDIRECT_URI_QUERY_PARAM = "login_redirect_uri";
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
const redirect_uri = useRouteQuery<string>("redirect_uri", "");
|
||||
const disabled = inject<Ref<boolean>>("disabled");
|
||||
|
||||
function handleSocialLogin() {
|
||||
if (disabled) {
|
||||
disabled.value = true;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
let authenticationUrl = props.authProvider.authenticationUrl;
|
||||
|
||||
if (redirect_uri.value) {
|
||||
authenticationUrl = `${authenticationUrl}?${REDIRECT_URI_QUERY_PARAM}=${redirect_uri.value}`;
|
||||
}
|
||||
|
||||
window.location.href = authenticationUrl;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="group inline-flex select-none flex-row items-center gap-2 rounded bg-white px-2.5 py-1.5 ring-1 ring-gray-200 transition-all hover:bg-gray-100 hover:shadow hover:ring-gray-900"
|
||||
:class="{
|
||||
'cursor-not-allowed opacity-80 hover:shadow-none hover:ring-gray-200':
|
||||
disabled,
|
||||
}"
|
||||
:disabled="disabled"
|
||||
@click="handleSocialLogin"
|
||||
>
|
||||
<svg
|
||||
v-if="loading"
|
||||
class="h-4 w-4 animate-spin"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<circle
|
||||
class="opacity-25"
|
||||
cx="12"
|
||||
cy="12"
|
||||
r="10"
|
||||
stroke="currentColor"
|
||||
stroke-width="4"
|
||||
></circle>
|
||||
<path
|
||||
class="opacity-75"
|
||||
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"
|
||||
fill="currentColor"
|
||||
></path>
|
||||
</svg>
|
||||
|
||||
<img
|
||||
v-else
|
||||
:alt="authProvider.displayName"
|
||||
class="h-4 w-4 rounded-full"
|
||||
:src="authProvider.logo"
|
||||
/>
|
||||
|
||||
<span class="text-xs text-gray-800 group-hover:text-gray-900">
|
||||
{{ authProvider.displayName }}
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
|
@ -1,34 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import type { Ref } from "vue";
|
||||
import { provide, ref } from "vue";
|
||||
|
||||
// auth providers
|
||||
import { useGlobalInfoFetch } from "@console/composables/use-global-info";
|
||||
import SocialAuthProviderItem from "./SocialAuthProviderItem.vue";
|
||||
|
||||
const { globalInfo } = useGlobalInfoFetch();
|
||||
|
||||
provide<Ref<boolean>>("disabled", ref(false));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Transition v-if="globalInfo?.socialAuthProviders.length" appear name="fade">
|
||||
<div>
|
||||
<div
|
||||
class="my-4 flex items-center before:ml-1 before:mt-0.5 before:flex-1 before:border-t before:border-gray-200 after:mr-1 after:mt-0.5 after:flex-1 after:border-t after:border-gray-200"
|
||||
>
|
||||
<p class="mx-4 mb-0 text-center text-xs dark:text-neutral-600">
|
||||
{{ $t("core.components.social_auth_providers.title") }}
|
||||
</p>
|
||||
</div>
|
||||
<ul class="flex flex-row flex-wrap justify-center gap-2">
|
||||
<li
|
||||
v-for="(socialAuthProvider, index) in globalInfo.socialAuthProviders"
|
||||
:key="index"
|
||||
>
|
||||
<SocialAuthProviderItem :auth-provider="socialAuthProvider" />
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Transition>
|
||||
</template>
|
|
@ -1,260 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { useGlobalInfoStore } from "@/stores/global-info";
|
||||
import { submitForm } from "@formkit/core";
|
||||
import { publicApiClient } from "@halo-dev/api-client";
|
||||
import { Toast, VButton } from "@halo-dev/components";
|
||||
import { useMutation } from "@tanstack/vue-query";
|
||||
import { useRouteQuery } from "@vueuse/router";
|
||||
import { useIntervalFn } from "@vueuse/shared";
|
||||
import { computed, onMounted, reactive, ref, type ComputedRef } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
withDefaults(
|
||||
defineProps<{
|
||||
buttonText?: string;
|
||||
}>(),
|
||||
{
|
||||
buttonText: "core.signup.operations.submit.button",
|
||||
}
|
||||
);
|
||||
|
||||
const formState = ref({
|
||||
password: "",
|
||||
user: {
|
||||
apiVersion: "v1alpha1",
|
||||
kind: "User",
|
||||
metadata: {
|
||||
name: "",
|
||||
},
|
||||
spec: {
|
||||
avatar: "",
|
||||
displayName: "",
|
||||
email: "",
|
||||
},
|
||||
},
|
||||
verifyCode: "",
|
||||
});
|
||||
const loading = ref(false);
|
||||
|
||||
const emit = defineEmits<{
|
||||
(event: "succeed"): void;
|
||||
}>();
|
||||
|
||||
const login = useRouteQuery<string>("login");
|
||||
const name = useRouteQuery<string>("name");
|
||||
const globalInfoStore = useGlobalInfoStore();
|
||||
const signUpCond = reactive({
|
||||
mustVerifyEmailOnRegistration: false,
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
signUpCond.mustVerifyEmailOnRegistration =
|
||||
globalInfoStore.globalInfo?.mustVerifyEmailOnRegistration || false;
|
||||
if (login.value) {
|
||||
formState.value.user.metadata.name = login.value;
|
||||
}
|
||||
if (name.value) {
|
||||
formState.value.user.spec.displayName = name.value;
|
||||
}
|
||||
});
|
||||
const emailRegex = new RegExp("^[\\w\\-.]+@([\\w-]+\\.)+[\\w-]{2,}$");
|
||||
const emailValidation: ComputedRef<
|
||||
// please see https://github.com/formkit/formkit/blob/bd5cf1c378d358ed3aba7b494713af20b6c909ab/packages/inputs/src/props.ts#L660
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
string | Array<[rule: string, ...args: any]>
|
||||
> = computed(() => {
|
||||
if (signUpCond.mustVerifyEmailOnRegistration)
|
||||
return [["required"], ["matches", emailRegex]];
|
||||
else return "required|email|length:0,100";
|
||||
});
|
||||
|
||||
const handleSignup = async () => {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
await publicApiClient.user.signUp({
|
||||
signUpRequest: formState.value,
|
||||
});
|
||||
|
||||
Toast.success(t("core.signup.operations.submit.toast_success"));
|
||||
|
||||
emit("succeed");
|
||||
} catch (error) {
|
||||
console.error("Failed to sign up", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
const inputClasses = {
|
||||
outer: "!py-3 first:!pt-0 last:!pb-0",
|
||||
};
|
||||
|
||||
// the code below is copied from console/uc-src/modules/profile/components/EmailVerifyModal.vue
|
||||
const timer = ref(0);
|
||||
const { pause, resume, isActive } = useIntervalFn(
|
||||
() => {
|
||||
if (timer.value <= 0) {
|
||||
pause();
|
||||
} else {
|
||||
timer.value--;
|
||||
}
|
||||
},
|
||||
1000,
|
||||
{
|
||||
immediate: false,
|
||||
}
|
||||
);
|
||||
|
||||
const { mutate: sendVerifyCode, isLoading: isSending } = useMutation({
|
||||
mutationKey: ["send-verify-code"],
|
||||
mutationFn: async () => {
|
||||
if (!formState.value.user.spec.email.match(emailRegex)) {
|
||||
Toast.error(t("core.signup.fields.email.matchFailed"));
|
||||
throw new Error("email is illegal");
|
||||
}
|
||||
return await publicApiClient.user.sendRegisterVerifyEmail({
|
||||
registerVerifyEmailRequest: {
|
||||
email: formState.value.user.spec.email,
|
||||
},
|
||||
});
|
||||
},
|
||||
onSuccess() {
|
||||
Toast.success(
|
||||
t("core.signup.fields.verify_code.operations.send_code.toast_success")
|
||||
);
|
||||
timer.value = 60;
|
||||
resume();
|
||||
},
|
||||
});
|
||||
|
||||
const sendVerifyCodeButtonText = computed(() => {
|
||||
if (isSending.value) {
|
||||
return t(
|
||||
"core.signup.fields.verify_code.operations.send_code.buttons.sending"
|
||||
);
|
||||
}
|
||||
return isActive.value
|
||||
? t(
|
||||
"core.signup.fields.verify_code.operations.send_code.buttons.countdown",
|
||||
{
|
||||
timer: timer.value,
|
||||
}
|
||||
)
|
||||
: t("core.signup.fields.verify_code.operations.send_code.buttons.send");
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<FormKit
|
||||
id="signup-form"
|
||||
name="signup-form"
|
||||
:actions="false"
|
||||
:classes="{
|
||||
form: '!divide-none',
|
||||
}"
|
||||
type="form"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
@submit="handleSignup"
|
||||
@keyup.enter="submitForm('signup-form')"
|
||||
>
|
||||
<FormKit
|
||||
v-model="formState.user.metadata.name"
|
||||
name="username"
|
||||
:placeholder="$t('core.signup.fields.username.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.username.placeholder')"
|
||||
:classes="inputClasses"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
:validation="[
|
||||
['required'],
|
||||
['length', '4', '63'],
|
||||
[
|
||||
'matches',
|
||||
/^[a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*$/,
|
||||
],
|
||||
]"
|
||||
:validation-messages="{
|
||||
matches: $t('core.user.editing_modal.fields.username.validation'),
|
||||
}"
|
||||
>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
v-model="formState.user.spec.displayName"
|
||||
name="displayName"
|
||||
:placeholder="$t('core.signup.fields.display_name.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.display_name.placeholder')"
|
||||
:classes="inputClasses"
|
||||
type="text"
|
||||
validation="required"
|
||||
>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
v-model="formState.user.spec.email"
|
||||
:placeholder="$t('core.signup.fields.email.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.email.placeholder')"
|
||||
type="email"
|
||||
name="email"
|
||||
:validation="emailValidation"
|
||||
:validation-messages="{
|
||||
matches: $t('core.signup.fields.email.matchFailed'),
|
||||
}"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-if="signUpCond.mustVerifyEmailOnRegistration"
|
||||
v-model="formState.verifyCode"
|
||||
type="number"
|
||||
name="code"
|
||||
:placeholder="$t('core.signup.fields.verify_code.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.verify_code.placeholder')"
|
||||
validation="required"
|
||||
>
|
||||
<template #suffix>
|
||||
<VButton
|
||||
:loading="isSending"
|
||||
:disabled="isActive"
|
||||
class="rounded-none border-y-0 border-l border-r-0 tabular-nums"
|
||||
@click="sendVerifyCode"
|
||||
>
|
||||
{{ sendVerifyCodeButtonText }}
|
||||
</VButton>
|
||||
</template>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
v-model="formState.password"
|
||||
name="password"
|
||||
:placeholder="$t('core.signup.fields.password.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.password.placeholder')"
|
||||
:classes="inputClasses"
|
||||
type="password"
|
||||
validation="required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
}"
|
||||
>
|
||||
</FormKit>
|
||||
<FormKit
|
||||
name="password_confirm"
|
||||
:placeholder="$t('core.signup.fields.password_confirm.placeholder')"
|
||||
:validation-label="$t('core.signup.fields.password_confirm.placeholder')"
|
||||
:classes="inputClasses"
|
||||
type="password"
|
||||
validation="confirm|required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
}"
|
||||
>
|
||||
</FormKit>
|
||||
</FormKit>
|
||||
<VButton
|
||||
class="mt-8"
|
||||
block
|
||||
type="secondary"
|
||||
:loading="loading"
|
||||
@click="submitForm('signup-form')"
|
||||
>
|
||||
{{ $t(buttonText) }}
|
||||
</VButton>
|
||||
</template>
|
|
@ -1,13 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex h-screen flex-col items-center overflow-auto bg-white/90 pt-[30vh]"
|
||||
>
|
||||
<IconLogo class="mb-8 flex-none" />
|
||||
|
||||
<RouterView />
|
||||
</div>
|
||||
</template>
|
|
@ -1,78 +1,4 @@
|
|||
core:
|
||||
login:
|
||||
title: Login
|
||||
fields:
|
||||
username:
|
||||
placeholder: Username
|
||||
password:
|
||||
placeholder: Password
|
||||
remember_me:
|
||||
label: Remember Me
|
||||
operations:
|
||||
submit:
|
||||
toast_success: Login successful
|
||||
toast_failed: Login failed, incorrect username or password
|
||||
toast_csrf: CSRF Token expired, please try again
|
||||
signup:
|
||||
label: No account
|
||||
button: Sign up
|
||||
return_login:
|
||||
label: Already have an account
|
||||
button: Sign in
|
||||
return_site: Return to site
|
||||
reset_password:
|
||||
button: Retrieve password
|
||||
button: Login
|
||||
modal:
|
||||
title: Re-login
|
||||
2fa:
|
||||
fields:
|
||||
code:
|
||||
placeholder: Two-step verification code
|
||||
label: Two-step verification code
|
||||
signup:
|
||||
title: Sign up
|
||||
fields:
|
||||
username:
|
||||
placeholder: Username
|
||||
display_name:
|
||||
placeholder: Display name
|
||||
email:
|
||||
placeholder: Email
|
||||
matchFailed: The email format is wrong or the service provider is not supported.
|
||||
verify_code:
|
||||
placeholder: Verification code
|
||||
operations:
|
||||
send_code:
|
||||
buttons:
|
||||
sending: sending
|
||||
send: Send Code
|
||||
countdown: resend after {timer} seconds
|
||||
toast_success: verification code sent
|
||||
toast_email_empty: please enter your email address
|
||||
password:
|
||||
placeholder: Password
|
||||
password_confirm:
|
||||
placeholder: Confirm password
|
||||
operations:
|
||||
submit:
|
||||
button: Sign up
|
||||
toast_success: Sign up successfully
|
||||
binding:
|
||||
title: Account binding
|
||||
common:
|
||||
toast:
|
||||
mounted: >-
|
||||
The current login method is not bound to an account, Please bind or
|
||||
sign up a new account first
|
||||
operations:
|
||||
login_and_bind:
|
||||
button: Login and Bind
|
||||
signup_and_bind:
|
||||
button: Signup and Bind
|
||||
bind:
|
||||
toast_success: Binding successfully
|
||||
toast_failed: Binding failed, no enabled login method found
|
||||
sidebar:
|
||||
search:
|
||||
placeholder: Search
|
||||
|
@ -1657,8 +1583,6 @@ core:
|
|||
page_label: page
|
||||
size_label: items per page
|
||||
total_label: Total {total} items
|
||||
social_auth_providers:
|
||||
title: Third-party login
|
||||
app_download_alert:
|
||||
description: >-
|
||||
Themes and plugins for Halo can be downloaded at the following
|
||||
|
@ -1871,32 +1795,6 @@ core:
|
|||
setting_modal:
|
||||
title: Post settings
|
||||
title: My posts
|
||||
uc_reset_password:
|
||||
fields:
|
||||
username:
|
||||
label: username
|
||||
password:
|
||||
label: New Password
|
||||
password_confirm:
|
||||
label: Confirm Password
|
||||
operations:
|
||||
reset:
|
||||
button: Reset Password
|
||||
toast_success: Reset successful
|
||||
title: Reset password
|
||||
reset_password:
|
||||
fields:
|
||||
username:
|
||||
label: Username
|
||||
email:
|
||||
label: email address
|
||||
operations:
|
||||
send:
|
||||
label: Send verification email
|
||||
toast_success: >-
|
||||
If your username and email address match, we will send an email to
|
||||
your email address.
|
||||
title: Reset password
|
||||
tool:
|
||||
title: Tools
|
||||
empty:
|
||||
|
|
|
@ -1,60 +1,4 @@
|
|||
core:
|
||||
login:
|
||||
title: Inicio de sesión
|
||||
fields:
|
||||
username:
|
||||
placeholder: Usuario
|
||||
password:
|
||||
placeholder: Contraseña
|
||||
operations:
|
||||
submit:
|
||||
toast_success: Inicio de sesión exitoso
|
||||
toast_failed: >-
|
||||
Error en el inicio de sesión, nombre de usuario o contraseña
|
||||
incorrectos
|
||||
toast_csrf: Token CSRF no válido, por favor inténtalo de nuevo
|
||||
signup:
|
||||
label: No tienes una cuenta
|
||||
button: Registrarse ahora
|
||||
return_login:
|
||||
label: Ya tienes una cuenta
|
||||
button: Iniciar sesión ahora
|
||||
return_site: Volver a la página de inicio
|
||||
button: Iniciar sesión
|
||||
modal:
|
||||
title: Volver a iniciar sesión
|
||||
signup:
|
||||
title: Registrarse
|
||||
fields:
|
||||
username:
|
||||
placeholder: Nombre de usuario
|
||||
display_name:
|
||||
placeholder: Nombre para mostrar
|
||||
password:
|
||||
placeholder: Contraseña
|
||||
password_confirm:
|
||||
placeholder: Confirmar contraseña
|
||||
operations:
|
||||
submit:
|
||||
button: Registrarse
|
||||
toast_success: Registrado exitosamente
|
||||
binding:
|
||||
title: Vinculación de cuentas
|
||||
common:
|
||||
toast:
|
||||
mounted: >-
|
||||
El método de inicio de sesión actual no está vinculado a una cuenta.
|
||||
Por favor, vincula o registra una nueva cuenta primero.
|
||||
operations:
|
||||
login_and_bind:
|
||||
button: Iniciar sesión y vincular
|
||||
signup_and_bind:
|
||||
button: Registrarse y vincular
|
||||
bind:
|
||||
toast_success: Vinculación exitosa
|
||||
toast_failed: >-
|
||||
Vinculación fallida, no se encontró ningún método de inicio de sesión
|
||||
habilitado.
|
||||
sidebar:
|
||||
search:
|
||||
placeholder: Buscar
|
||||
|
@ -1285,8 +1229,6 @@ core:
|
|||
page_label: página
|
||||
size_label: elementos por página
|
||||
total_label: Total de {total} elementos
|
||||
social_auth_providers:
|
||||
title: Inicio de sesión de terceros
|
||||
app_download_alert:
|
||||
description: >-
|
||||
Los temas y complementos para Halo se pueden descargar en las siguientes
|
||||
|
|
|
@ -1,76 +1,4 @@
|
|||
core:
|
||||
login:
|
||||
title: 登录
|
||||
fields:
|
||||
username:
|
||||
placeholder: 用户名
|
||||
password:
|
||||
placeholder: 密码
|
||||
remember_me:
|
||||
label: 保持登录会话
|
||||
operations:
|
||||
submit:
|
||||
toast_success: 登录成功
|
||||
toast_failed: 登录失败,用户名或密码错误
|
||||
toast_csrf: CSRF Token 失效,请重新尝试
|
||||
signup:
|
||||
label: 没有账号
|
||||
button: 立即注册
|
||||
return_login:
|
||||
label: 已有账号
|
||||
button: 立即登录
|
||||
return_site: 返回到首页
|
||||
reset_password:
|
||||
button: 找回密码
|
||||
button: 登录
|
||||
modal:
|
||||
title: 重新登录
|
||||
2fa:
|
||||
fields:
|
||||
code:
|
||||
placeholder: 请输入两步验证码
|
||||
label: 两步验证码
|
||||
signup:
|
||||
title: 注册
|
||||
fields:
|
||||
username:
|
||||
placeholder: 用户名
|
||||
display_name:
|
||||
placeholder: 名称
|
||||
email:
|
||||
placeholder: 电子邮箱
|
||||
matchFailed: 邮箱格式错误或服务商不受支持
|
||||
verify_code:
|
||||
placeholder: 验证码
|
||||
operations:
|
||||
send_code:
|
||||
buttons:
|
||||
sending: 发送中
|
||||
send: 发送验证码
|
||||
countdown: "{timer} 秒后重发"
|
||||
toast_success: 验证码已发送
|
||||
toast_email_empty: 请输入电子邮箱
|
||||
password:
|
||||
placeholder: 密码
|
||||
password_confirm:
|
||||
placeholder: 确认密码
|
||||
operations:
|
||||
submit:
|
||||
button: 注册
|
||||
toast_success: 注册成功
|
||||
binding:
|
||||
title: 账号绑定
|
||||
common:
|
||||
toast:
|
||||
mounted: 当前登录方式未绑定账号,请先绑定或注册新账号
|
||||
operations:
|
||||
login_and_bind:
|
||||
button: 登录并绑定
|
||||
signup_and_bind:
|
||||
button: 注册并绑定
|
||||
bind:
|
||||
toast_success: 绑定成功
|
||||
toast_failed: 绑定失败,没有找到已启用的登录方式
|
||||
sidebar:
|
||||
search:
|
||||
placeholder: 搜索
|
||||
|
@ -1413,30 +1341,6 @@ core:
|
|||
label: 密码
|
||||
confirm_password:
|
||||
label: 确认密码
|
||||
reset_password:
|
||||
title: 重置密码
|
||||
fields:
|
||||
username:
|
||||
label: 用户名
|
||||
email:
|
||||
label: 邮箱地址
|
||||
operations:
|
||||
send:
|
||||
label: 发送验证邮件
|
||||
toast_success: 如果你的用户名和邮箱地址匹配,我们将会发送一封邮件到你的邮箱。
|
||||
uc_reset_password:
|
||||
title: 重置密码
|
||||
fields:
|
||||
username:
|
||||
label: 用户名
|
||||
password:
|
||||
label: 新密码
|
||||
password_confirm:
|
||||
label: 确认密码
|
||||
operations:
|
||||
reset:
|
||||
button: 重置密码
|
||||
toast_success: 重置成功
|
||||
rbac:
|
||||
Attachments Management: 附件
|
||||
Attachment Manage: 附件管理
|
||||
|
@ -1575,8 +1479,6 @@ core:
|
|||
page_label: 页
|
||||
size_label: 条 / 页
|
||||
total_label: 共 {total} 项数据
|
||||
social_auth_providers:
|
||||
title: 三方登录
|
||||
app_download_alert:
|
||||
description: Halo 的主题和插件可以在以下地址下载:
|
||||
sources:
|
||||
|
|
|
@ -1,76 +1,4 @@
|
|||
core:
|
||||
login:
|
||||
title: 登入
|
||||
fields:
|
||||
username:
|
||||
placeholder: 用戶名
|
||||
password:
|
||||
placeholder: 密碼
|
||||
remember_me:
|
||||
label: 保持登入會話
|
||||
operations:
|
||||
submit:
|
||||
toast_success: 登入成功
|
||||
toast_failed: 登入失敗,用戶名或密碼錯誤
|
||||
toast_csrf: CSRF Token 失效,請重新嘗試
|
||||
signup:
|
||||
label: 沒有帳號
|
||||
button: 立即註冊
|
||||
return_login:
|
||||
label: 已有帳號
|
||||
button: 立即登入
|
||||
return_site: 返回到首頁
|
||||
reset_password:
|
||||
button: 找回密碼
|
||||
button: 登入
|
||||
modal:
|
||||
title: 重新登入
|
||||
2fa:
|
||||
fields:
|
||||
code:
|
||||
placeholder: 請輸入兩步驟驗證碼
|
||||
label: 兩步驟驗證碼
|
||||
signup:
|
||||
title: 註冊
|
||||
fields:
|
||||
username:
|
||||
placeholder: 用戶名
|
||||
display_name:
|
||||
placeholder: 名稱
|
||||
email:
|
||||
placeholder: 電子郵箱
|
||||
matchFailed: 郵箱格式錯誤或服務商不受支援
|
||||
verify_code:
|
||||
placeholder: 驗證碼
|
||||
operations:
|
||||
send_code:
|
||||
buttons:
|
||||
countdown: "{timer} 秒後重發"
|
||||
send: 發送驗證碼
|
||||
sending: 發送中
|
||||
toast_email_empty: 請輸入電子郵件信箱
|
||||
toast_success: 驗證碼已發送
|
||||
password:
|
||||
placeholder: 密碼
|
||||
password_confirm:
|
||||
placeholder: 確認密碼
|
||||
operations:
|
||||
submit:
|
||||
button: 註冊
|
||||
toast_success: 註冊成功
|
||||
binding:
|
||||
title: 帳號綁定
|
||||
common:
|
||||
toast:
|
||||
mounted: 當前登入方式未綁定帳號,請先綁定或註冊新帳號
|
||||
operations:
|
||||
login_and_bind:
|
||||
button: 登入並綁定
|
||||
signup_and_bind:
|
||||
button: 註冊並綁定
|
||||
bind:
|
||||
toast_success: 綁定成功
|
||||
toast_failed: 綁定失敗,沒有找到已啟用的登入方式
|
||||
sidebar:
|
||||
search:
|
||||
placeholder: 搜尋
|
||||
|
@ -1530,8 +1458,6 @@ core:
|
|||
page_label: 頁
|
||||
size_label: 條 / 頁
|
||||
total_label: 共 {total} 項資料
|
||||
social_auth_providers:
|
||||
title: 三方登入
|
||||
app_download_alert:
|
||||
description: Halo 的主題和插件可以在以下地址下載:
|
||||
sources:
|
||||
|
@ -1735,30 +1661,6 @@ core:
|
|||
setting_modal:
|
||||
title: 文章設定
|
||||
title: 我的文章
|
||||
uc_reset_password:
|
||||
fields:
|
||||
username:
|
||||
label: 用戶名
|
||||
password:
|
||||
label: 新密碼
|
||||
password_confirm:
|
||||
label: 確認密碼
|
||||
operations:
|
||||
reset:
|
||||
button: 重設密碼
|
||||
toast_success: 重置成功
|
||||
title: 重設密碼
|
||||
reset_password:
|
||||
fields:
|
||||
username:
|
||||
label: 使用者名稱
|
||||
email:
|
||||
label: 郵件地址
|
||||
operations:
|
||||
send:
|
||||
label: 發送驗證郵件
|
||||
toast_success: 如果你的用戶名和郵箱地址匹配,我們將會發送一封郵件到你的郵箱。
|
||||
title: 重設密碼
|
||||
tool:
|
||||
title: 工具
|
||||
empty:
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { i18n } from "@/locales";
|
||||
import { useUserStore } from "@/stores/user";
|
||||
import { axiosInstance } from "@halo-dev/api-client";
|
||||
import { Toast } from "@halo-dev/components";
|
||||
import type { AxiosError } from "axios";
|
||||
|
@ -45,10 +44,10 @@ export function setupApiClient() {
|
|||
const { title, detail } = errorResponse.data;
|
||||
|
||||
if (status === 401) {
|
||||
const userStore = useUserStore();
|
||||
userStore.loginModalVisible = true;
|
||||
Toast.warning(i18n.global.t("core.common.toast.login_expired"));
|
||||
|
||||
// TODO: show dialog
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
|
|
|
@ -6,7 +6,6 @@ interface UserStoreState {
|
|||
currentUser?: User;
|
||||
currentRoles?: Role[];
|
||||
isAnonymous: boolean;
|
||||
loginModalVisible: boolean;
|
||||
}
|
||||
|
||||
export const useUserStore = defineStore("user", {
|
||||
|
@ -14,7 +13,6 @@ export const useUserStore = defineStore("user", {
|
|||
currentUser: undefined,
|
||||
currentRoles: [],
|
||||
isAnonymous: true,
|
||||
loginModalVisible: false,
|
||||
}),
|
||||
actions: {
|
||||
async fetchCurrentUser() {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
<script lang="ts" setup>
|
||||
import LoginModal from "@/components/login/LoginModal.vue";
|
||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||
import { rbacAnnotations } from "@/constants/annotations";
|
||||
|
@ -280,7 +279,6 @@ const disallowAccessConsole = computed(() => {
|
|||
</Teleport>
|
||||
</div>
|
||||
</div>
|
||||
<LoginModal />
|
||||
</template>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import GatewayLayout from "@/layouts/GatewayLayout.vue";
|
||||
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
||||
import NotFound from "@/views/exceptions/NotFound.vue";
|
||||
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
||||
import ResetPassword from "@uc/views/ResetPassword.vue";
|
||||
import type { RouteRecordRaw } from "vue-router";
|
||||
|
||||
export const routes: Array<RouteRecordRaw> = [
|
||||
|
@ -22,20 +20,6 @@ export const routes: Array<RouteRecordRaw> = [
|
|||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
path: "/reset-password/:username",
|
||||
component: GatewayLayout,
|
||||
children: [
|
||||
{
|
||||
path: "",
|
||||
name: "ResetPassword",
|
||||
component: ResetPassword,
|
||||
meta: {
|
||||
title: "core.uc_reset_password.title",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
|
|
@ -1,108 +0,0 @@
|
|||
<script lang="ts" setup>
|
||||
import { publicApiClient } from "@halo-dev/api-client";
|
||||
import { Toast, VButton } from "@halo-dev/components";
|
||||
import { useRouteParams, useRouteQuery } from "@vueuse/router";
|
||||
import { ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const username = useRouteParams<string>("username");
|
||||
const token = useRouteQuery<string>("reset_password_token");
|
||||
|
||||
interface ResetPasswordForm {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
const loading = ref(false);
|
||||
|
||||
async function onSubmit(data: ResetPasswordForm) {
|
||||
try {
|
||||
loading.value = true;
|
||||
|
||||
await publicApiClient.user.resetPasswordByToken({
|
||||
name: data.username,
|
||||
resetPasswordRequest: {
|
||||
newPassword: data.password,
|
||||
token: token.value,
|
||||
},
|
||||
});
|
||||
|
||||
Toast.success(t("core.uc_reset_password.operations.reset.toast_success"));
|
||||
|
||||
window.location.href = "/console/login";
|
||||
} catch (error) {
|
||||
console.error("Failed to reset password", error);
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
const inputClasses = {
|
||||
outer: "!py-3 first:!pt-0 last:!pb-0",
|
||||
};
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex w-72 flex-col">
|
||||
<FormKit
|
||||
id="reset-password-form"
|
||||
name="reset-password-form"
|
||||
type="form"
|
||||
:classes="{
|
||||
form: '!divide-none',
|
||||
}"
|
||||
:config="{ validationVisibility: 'submit' }"
|
||||
@submit="onSubmit"
|
||||
@keyup.enter="$formkit.submit('reset-password-form')"
|
||||
>
|
||||
<FormKit
|
||||
:classes="inputClasses"
|
||||
name="username"
|
||||
:model-value="username"
|
||||
:placeholder="$t('core.uc_reset_password.fields.username.label')"
|
||||
:validation-label="$t('core.uc_reset_password.fields.username.label')"
|
||||
:autofocus="true"
|
||||
type="text"
|
||||
disabled
|
||||
validation="required"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
:classes="inputClasses"
|
||||
name="password"
|
||||
type="password"
|
||||
:placeholder="$t('core.uc_reset_password.fields.password.label')"
|
||||
:validation-label="$t('core.uc_reset_password.fields.password.label')"
|
||||
validation="required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
}"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
:classes="inputClasses"
|
||||
name="password_confirm"
|
||||
type="password"
|
||||
:placeholder="
|
||||
$t('core.uc_reset_password.fields.password_confirm.label')
|
||||
"
|
||||
:validation-label="
|
||||
$t('core.uc_reset_password.fields.password_confirm.label')
|
||||
"
|
||||
validation="confirm|required:trim|length:5,100|matches:/^\S.*\S$/"
|
||||
:validation-messages="{
|
||||
matches: $t('core.formkit.validation.trim'),
|
||||
}"
|
||||
></FormKit>
|
||||
</FormKit>
|
||||
<VButton
|
||||
class="mt-8"
|
||||
block
|
||||
:loading="loading"
|
||||
type="secondary"
|
||||
@click="$formkit.submit('reset-password-form')"
|
||||
>
|
||||
{{ $t("core.uc_reset_password.operations.reset.button") }}
|
||||
</VButton>
|
||||
</div>
|
||||
</template>
|
Loading…
Reference in New Issue