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
Ryan Wang 2024-09-28 18:19:41 +08:00 committed by GitHub
parent a4c906706f
commit d5233963fb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
25 changed files with 9 additions and 1607 deletions

View File

@ -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">

View File

@ -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;
}

View File

@ -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 || {};

View File

@ -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;

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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:

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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);
}

View File

@ -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() {

View File

@ -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">

View File

@ -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;

View File

@ -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>