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>
|
<script lang="ts" setup>
|
||||||
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
|
||||||
import LoginModal from "@/components/login/LoginModal.vue";
|
|
||||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
|
@ -303,7 +302,6 @@ onMounted(() => {
|
||||||
v-if="globalSearchVisible"
|
v-if="globalSearchVisible"
|
||||||
@close="globalSearchVisible = false"
|
@close="globalSearchVisible = false"
|
||||||
/>
|
/>
|
||||||
<LoginModal />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -1,11 +1,10 @@
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
|
||||||
import { useUserStore } from "@/stores/user";
|
import { useUserStore } from "@/stores/user";
|
||||||
import type { Router } from "vue-router";
|
import type { Router } from "vue-router";
|
||||||
|
|
||||||
const whiteList = ["Setup", "Login", "Binding", "ResetPassword", "Redirect"];
|
const whiteList = ["Setup"];
|
||||||
|
|
||||||
export function setupAuthCheckGuard(router: Router) {
|
export function setupAuthCheckGuard(router: Router) {
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, _, next) => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
|
|
||||||
if (userStore.isAnonymous) {
|
if (userStore.isAnonymous) {
|
||||||
|
@ -13,67 +12,9 @@ export function setupAuthCheckGuard(router: Router) {
|
||||||
next();
|
next();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
window.location.href = `/login?redirect_uri=${encodeURIComponent(
|
||||||
next({
|
window.location.href
|
||||||
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();
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { useUserStore } from "@/stores/user";
|
||||||
import type { Router } from "vue-router";
|
import type { Router } from "vue-router";
|
||||||
|
|
||||||
export function setupCheckStatesGuard(router: Router) {
|
export function setupCheckStatesGuard(router: Router) {
|
||||||
router.beforeEach(async (to, from, next) => {
|
router.beforeEach(async (to, _, next) => {
|
||||||
const userStore = useUserStore();
|
const userStore = useUserStore();
|
||||||
const { globalInfo } = useGlobalInfoStore();
|
const { globalInfo } = useGlobalInfoStore();
|
||||||
const { userInitialized, dataInitialized } = globalInfo || {};
|
const { userInitialized, dataInitialized } = globalInfo || {};
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { hasPermission } from "@/utils/permission";
|
||||||
import type { Router } from "vue-router";
|
import type { Router } from "vue-router";
|
||||||
|
|
||||||
export function setupPermissionGuard(router: Router) {
|
export function setupPermissionGuard(router: Router) {
|
||||||
router.beforeEach((to, from, next) => {
|
router.beforeEach((to, _, next) => {
|
||||||
const roleStore = useRoleStore();
|
const roleStore = useRoleStore();
|
||||||
const { uiPermissions } = roleStore.permissions;
|
const { uiPermissions } = roleStore.permissions;
|
||||||
const { meta } = to;
|
const { meta } = to;
|
||||||
|
|
|
@ -1,11 +1,6 @@
|
||||||
import GatewayLayout from "@/layouts/GatewayLayout.vue";
|
|
||||||
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
||||||
import NotFound from "@/views/exceptions/NotFound.vue";
|
import NotFound from "@/views/exceptions/NotFound.vue";
|
||||||
import BasicLayout from "@console/layouts/BasicLayout.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 Setup from "@console/views/system/Setup.vue";
|
||||||
import SetupInitialData from "@console/views/system/SetupInitialData.vue";
|
import SetupInitialData from "@console/views/system/SetupInitialData.vue";
|
||||||
import type { RouteRecordRaw } from "vue-router";
|
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",
|
path: "/setup",
|
||||||
component: Setup,
|
component: Setup,
|
||||||
|
@ -71,25 +38,6 @@ export const routes: Array<RouteRecordRaw> = [
|
||||||
title: "core.setup.title",
|
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;
|
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:
|
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:
|
sidebar:
|
||||||
search:
|
search:
|
||||||
placeholder: Search
|
placeholder: Search
|
||||||
|
@ -1657,8 +1583,6 @@ core:
|
||||||
page_label: page
|
page_label: page
|
||||||
size_label: items per page
|
size_label: items per page
|
||||||
total_label: Total {total} items
|
total_label: Total {total} items
|
||||||
social_auth_providers:
|
|
||||||
title: Third-party login
|
|
||||||
app_download_alert:
|
app_download_alert:
|
||||||
description: >-
|
description: >-
|
||||||
Themes and plugins for Halo can be downloaded at the following
|
Themes and plugins for Halo can be downloaded at the following
|
||||||
|
@ -1871,32 +1795,6 @@ core:
|
||||||
setting_modal:
|
setting_modal:
|
||||||
title: Post settings
|
title: Post settings
|
||||||
title: My posts
|
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:
|
tool:
|
||||||
title: Tools
|
title: Tools
|
||||||
empty:
|
empty:
|
||||||
|
|
|
@ -1,60 +1,4 @@
|
||||||
core:
|
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:
|
sidebar:
|
||||||
search:
|
search:
|
||||||
placeholder: Buscar
|
placeholder: Buscar
|
||||||
|
@ -1285,8 +1229,6 @@ core:
|
||||||
page_label: página
|
page_label: página
|
||||||
size_label: elementos por página
|
size_label: elementos por página
|
||||||
total_label: Total de {total} elementos
|
total_label: Total de {total} elementos
|
||||||
social_auth_providers:
|
|
||||||
title: Inicio de sesión de terceros
|
|
||||||
app_download_alert:
|
app_download_alert:
|
||||||
description: >-
|
description: >-
|
||||||
Los temas y complementos para Halo se pueden descargar en las siguientes
|
Los temas y complementos para Halo se pueden descargar en las siguientes
|
||||||
|
|
|
@ -1,76 +1,4 @@
|
||||||
core:
|
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:
|
sidebar:
|
||||||
search:
|
search:
|
||||||
placeholder: 搜索
|
placeholder: 搜索
|
||||||
|
@ -1413,30 +1341,6 @@ core:
|
||||||
label: 密码
|
label: 密码
|
||||||
confirm_password:
|
confirm_password:
|
||||||
label: 确认密码
|
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:
|
rbac:
|
||||||
Attachments Management: 附件
|
Attachments Management: 附件
|
||||||
Attachment Manage: 附件管理
|
Attachment Manage: 附件管理
|
||||||
|
@ -1575,8 +1479,6 @@ core:
|
||||||
page_label: 页
|
page_label: 页
|
||||||
size_label: 条 / 页
|
size_label: 条 / 页
|
||||||
total_label: 共 {total} 项数据
|
total_label: 共 {total} 项数据
|
||||||
social_auth_providers:
|
|
||||||
title: 三方登录
|
|
||||||
app_download_alert:
|
app_download_alert:
|
||||||
description: Halo 的主题和插件可以在以下地址下载:
|
description: Halo 的主题和插件可以在以下地址下载:
|
||||||
sources:
|
sources:
|
||||||
|
|
|
@ -1,76 +1,4 @@
|
||||||
core:
|
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:
|
sidebar:
|
||||||
search:
|
search:
|
||||||
placeholder: 搜尋
|
placeholder: 搜尋
|
||||||
|
@ -1530,8 +1458,6 @@ core:
|
||||||
page_label: 頁
|
page_label: 頁
|
||||||
size_label: 條 / 頁
|
size_label: 條 / 頁
|
||||||
total_label: 共 {total} 項資料
|
total_label: 共 {total} 項資料
|
||||||
social_auth_providers:
|
|
||||||
title: 三方登入
|
|
||||||
app_download_alert:
|
app_download_alert:
|
||||||
description: Halo 的主題和插件可以在以下地址下載:
|
description: Halo 的主題和插件可以在以下地址下載:
|
||||||
sources:
|
sources:
|
||||||
|
@ -1735,30 +1661,6 @@ core:
|
||||||
setting_modal:
|
setting_modal:
|
||||||
title: 文章設定
|
title: 文章設定
|
||||||
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:
|
tool:
|
||||||
title: 工具
|
title: 工具
|
||||||
empty:
|
empty:
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { i18n } from "@/locales";
|
import { i18n } from "@/locales";
|
||||||
import { useUserStore } from "@/stores/user";
|
|
||||||
import { axiosInstance } from "@halo-dev/api-client";
|
import { axiosInstance } from "@halo-dev/api-client";
|
||||||
import { Toast } from "@halo-dev/components";
|
import { Toast } from "@halo-dev/components";
|
||||||
import type { AxiosError } from "axios";
|
import type { AxiosError } from "axios";
|
||||||
|
@ -45,10 +44,10 @@ export function setupApiClient() {
|
||||||
const { title, detail } = errorResponse.data;
|
const { title, detail } = errorResponse.data;
|
||||||
|
|
||||||
if (status === 401) {
|
if (status === 401) {
|
||||||
const userStore = useUserStore();
|
|
||||||
userStore.loginModalVisible = true;
|
|
||||||
Toast.warning(i18n.global.t("core.common.toast.login_expired"));
|
Toast.warning(i18n.global.t("core.common.toast.login_expired"));
|
||||||
|
|
||||||
|
// TODO: show dialog
|
||||||
|
|
||||||
return Promise.reject(error);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ interface UserStoreState {
|
||||||
currentUser?: User;
|
currentUser?: User;
|
||||||
currentRoles?: Role[];
|
currentRoles?: Role[];
|
||||||
isAnonymous: boolean;
|
isAnonymous: boolean;
|
||||||
loginModalVisible: boolean;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const useUserStore = defineStore("user", {
|
export const useUserStore = defineStore("user", {
|
||||||
|
@ -14,7 +13,6 @@ export const useUserStore = defineStore("user", {
|
||||||
currentUser: undefined,
|
currentUser: undefined,
|
||||||
currentRoles: [],
|
currentRoles: [],
|
||||||
isAnonymous: true,
|
isAnonymous: true,
|
||||||
loginModalVisible: false,
|
|
||||||
}),
|
}),
|
||||||
actions: {
|
actions: {
|
||||||
async fetchCurrentUser() {
|
async fetchCurrentUser() {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import LoginModal from "@/components/login/LoginModal.vue";
|
|
||||||
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
import { RoutesMenu } from "@/components/menu/RoutesMenu";
|
||||||
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
import { useRouteMenuGenerator } from "@/composables/use-route-menu-generator";
|
||||||
import { rbacAnnotations } from "@/constants/annotations";
|
import { rbacAnnotations } from "@/constants/annotations";
|
||||||
|
@ -280,7 +279,6 @@ const disallowAccessConsole = computed(() => {
|
||||||
</Teleport>
|
</Teleport>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<LoginModal />
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import GatewayLayout from "@/layouts/GatewayLayout.vue";
|
|
||||||
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
import Forbidden from "@/views/exceptions/Forbidden.vue";
|
||||||
import NotFound from "@/views/exceptions/NotFound.vue";
|
import NotFound from "@/views/exceptions/NotFound.vue";
|
||||||
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
import BasicLayout from "@uc/layouts/BasicLayout.vue";
|
||||||
import ResetPassword from "@uc/views/ResetPassword.vue";
|
|
||||||
import type { RouteRecordRaw } from "vue-router";
|
import type { RouteRecordRaw } from "vue-router";
|
||||||
|
|
||||||
export const routes: Array<RouteRecordRaw> = [
|
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;
|
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