refactor: logic of user login (halo-dev/console#636)

#### What type of PR is this?

/kind improvement
/milestone 2.0

#### What this PR does / why we need it:

优化用户登录的逻辑。 适配:https://github.com/halo-dev/halo/pull/2528

#### Which issue(s) this PR fixes:

Fixes https://github.com/halo-dev/halo/issues/2506

#### Special notes for your reviewer:

#### Does this PR introduce a user-facing change?

```release-note
优化用户登录的逻辑
```
pull/3445/head
Ryan Wang 2022-10-11 17:00:14 +08:00 committed by GitHub
parent db39c3a1d7
commit 64aa137bab
8 changed files with 104 additions and 22 deletions

View File

@ -6,27 +6,49 @@ import {
VRoutesMenu, VRoutesMenu,
VTag, VTag,
VAvatar, VAvatar,
VSpace,
VButton,
useDialog,
} from "@halo-dev/components"; } from "@halo-dev/components";
import type { MenuGroupType, MenuItemType } from "../types/menus"; import type { MenuGroupType, MenuItemType } from "../types/menus";
import type { User } from "@halo-dev/api-client"; import type { User } from "@halo-dev/api-client";
import logo from "@/assets/logo.svg"; import logo from "@/assets/logo.svg";
import { RouterView, useRoute, useRouter } from "vue-router"; import { RouterView, useRoute, useRouter } from "vue-router";
import { computed, inject, ref, type Ref } from "vue"; import { computed, inject, ref, type Ref } from "vue";
import axios from "axios";
const menus = inject<MenuGroupType[]>("menus"); const menus = inject<MenuGroupType[]>("menus");
const minimenus = inject<MenuItemType[]>("minimenus"); const minimenus = inject<MenuItemType[]>("minimenus");
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
const dialog = useDialog();
const moreMenuVisible = ref(false); const moreMenuVisible = ref(false);
const moreMenuRootVisible = ref(false); const moreMenuRootVisible = ref(false);
const currentUser = inject<User>("currentUser"); const currentUser = inject<User>("currentUser");
const apiUrl = inject<string>("apiUrl");
const handleRouteToProfile = () => { const handleRouteToProfile = () => {
router.push({ path: `/users/${currentUser?.metadata.name}/detail` }); router.push({ path: `/users/${currentUser?.metadata.name}/detail` });
}; };
const handleLogout = () => {
dialog.warning({
title: "是否确认退出登录?",
onConfirm: async () => {
try {
await axios.post(`${apiUrl}/logout`, undefined, {
withCredentials: true,
});
router.replace({ name: "Login" });
} catch (error) {
console.error("Failed to logout", error);
}
},
});
};
const currentRole = computed(() => { const currentRole = computed(() => {
return JSON.parse( return JSON.parse(
currentUser?.metadata.annotations?.[ currentUser?.metadata.annotations?.[
@ -86,12 +108,33 @@ const isMac = /macintosh|mac os x/i.test(navigator.userAgent);
</VTag> </VTag>
</div> </div>
</div> </div>
<div <FloatingDropdown
class="profile-control cursor-pointer rounded p-1 transition-all hover:bg-gray-100" class="profile-control cursor-pointer rounded p-1 transition-all hover:bg-gray-100"
@click="handleRouteToProfile"
> >
<IconMore /> <IconMore />
</div> <template #popper>
<div class="w-48 p-2">
<VSpace class="w-full" direction="column">
<VButton
v-close-popper
block
type="secondary"
@click="handleRouteToProfile"
>
个人资料
</VButton>
<VButton
v-close-popper
block
type="default"
@click="handleLogout"
>
退出登录
</VButton>
</VSpace>
</div>
</template>
</FloatingDropdown>
</div> </div>
</aside> </aside>
<main class="content w-full overflow-y-auto pb-12 mb-safe md:pb-0"> <main class="content w-full overflow-y-auto pb-12 mb-safe md:pb-0">

View File

@ -12,7 +12,7 @@ import {
IconPages, IconPages,
IconUserSettings, IconUserSettings,
} from "@halo-dev/components"; } from "@halo-dev/components";
import { computed, markRaw, onMounted, ref, watch, type Component } from "vue"; import { computed, markRaw, ref, watch, type Component } from "vue";
import Fuse from "fuse.js"; import Fuse from "fuse.js";
import { apiClient } from "@/utils/api-client"; import { apiClient } from "@/utils/api-client";
import { usePermission } from "@/utils/permission"; import { usePermission } from "@/utils/permission";
@ -356,6 +356,8 @@ watch(
() => props.visible, () => props.visible,
(visible) => { (visible) => {
if (visible) { if (visible) {
handleBuildSearchIndex();
setTimeout(() => { setTimeout(() => {
globalSearchInput.value?.focus(); globalSearchInput.value?.focus();
}, 100); }, 100);
@ -369,10 +371,6 @@ watch(
} }
); );
onMounted(() => {
handleBuildSearchIndex();
});
const onVisibleChange = (visible: boolean) => { const onVisibleChange = (visible: boolean) => {
emit("update:visible", visible); emit("update:visible", visible);
}; };

View File

@ -214,6 +214,7 @@ async function initApp() {
} finally { } finally {
app.provide<MenuGroupType[]>("menus", menus); app.provide<MenuGroupType[]>("menus", menus);
app.provide<MenuItemType[]>("minimenus", minimenus); app.provide<MenuItemType[]>("minimenus", minimenus);
app.provide<string>("apiUrl", import.meta.env.VITE_API_URL);
app.use(router); app.use(router);
app.mount("#app"); app.mount("#app");

View File

@ -3,9 +3,12 @@ import { IconShieldUser, IconUserLine, VButton } from "@halo-dev/components";
import { v4 as uuid } from "uuid"; import { v4 as uuid } from "uuid";
import qs from "qs"; import qs from "qs";
import logo from "@/assets/logo.svg"; import logo from "@/assets/logo.svg";
import { onMounted, ref } from "vue"; import { inject, onBeforeMount, onMounted, ref } from "vue";
import { submitForm } from "@formkit/vue"; import { submitForm } from "@formkit/vue";
import router from "@/router"; import router from "@/router";
import axios from "axios";
import type { User } from "@halo-dev/api-client";
import { setFocus } from "@/formkit/utils/focus";
interface LoginForm { interface LoginForm {
_csrf: string; _csrf: string;
@ -18,6 +21,7 @@ const loginForm = ref<LoginForm>({
username: "", username: "",
password: "", password: "",
}); });
const loading = ref(false); const loading = ref(false);
const handleGenerateToken = async () => { const handleGenerateToken = async () => {
@ -29,24 +33,35 @@ const handleGenerateToken = async () => {
const handleLogin = async () => { const handleLogin = async () => {
try { try {
loading.value = true; loading.value = true;
await fetch(`${import.meta.env.VITE_API_URL}/login`, { await axios.post(
method: "POST", `${import.meta.env.VITE_API_URL}/login`,
headers: { qs.stringify(loginForm.value),
"Content-Type": "application/x-www-form-urlencoded", {
}, withCredentials: true,
credentials: "include", headers: {
redirect: "manual", "Content-Type": "application/x-www-form-urlencoded",
body: qs.stringify(loginForm.value), },
}); }
await router.push({ name: "Dashboard" }); );
await router.go(0); localStorage.setItem("logged_in", "true");
router.go(0);
} catch (e) { } catch (e) {
console.error("Failed to login", e); console.error("Failed to login", e);
alert("登录失败,用户名或密码错误");
loginForm.value.password = "";
setFocus("passwordInput");
} finally { } finally {
loading.value = false; loading.value = false;
} }
}; };
onBeforeMount(() => {
const currentUser = inject<User>("currentUser");
if (currentUser) {
router.push({ name: "Dashboard" });
}
});
onMounted(() => { onMounted(() => {
handleGenerateToken(); handleGenerateToken();
}); });
@ -61,7 +76,7 @@ onMounted(() => {
name="login-form" name="login-form"
:actions="false" :actions="false"
type="form" type="form"
:config="{ animation: 'none' }" :config="{ validationVisibility: 'submit' }"
@submit="handleLogin" @submit="handleLogin"
@keyup.enter="submitForm('login-form')" @keyup.enter="submitForm('login-form')"
> >
@ -79,6 +94,7 @@ onMounted(() => {
</template> </template>
</FormKit> </FormKit>
<FormKit <FormKit
id="passwordInput"
:validation-messages="{ :validation-messages="{
required: '请输入密码', required: '请输入密码',
}" }"
@ -92,7 +108,12 @@ onMounted(() => {
</template> </template>
</FormKit> </FormKit>
</FormKit> </FormKit>
<VButton block type="secondary" @click="submitForm('login-form')"> <VButton
block
:loading="loading"
type="secondary"
@click="submitForm('login-form')"
>
登录 登录
</VButton> </VButton>
</div> </div>

View File

@ -0,0 +1,15 @@
import type { Router } from "vue-router";
export function setupAuthCheckGuard(router: Router) {
router.beforeEach((to, from, next) => {
if (to.name === "Setup" || to.name === "Login") {
next();
return;
}
if (localStorage.getItem("logged_in") !== "true") {
next({ name: "Login" });
return;
}
next();
});
}

View File

@ -5,6 +5,7 @@ export function setupCheckStatesGuard(router: Router) {
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
if (to.name === "Setup" || to.name === "Login") { if (to.name === "Setup" || to.name === "Login") {
next(); next();
return;
} }
const systemStateStore = useSystemStatesStore(); const systemStateStore = useSystemStatesStore();

View File

@ -2,6 +2,7 @@ import { createRouter, createWebHashHistory } from "vue-router";
import routesConfig from "@/router/routes.config"; import routesConfig from "@/router/routes.config";
import { setupPermissionGuard } from "./guards/permission"; import { setupPermissionGuard } from "./guards/permission";
import { setupCheckStatesGuard } from "./guards/check-states"; import { setupCheckStatesGuard } from "./guards/check-states";
import { setupAuthCheckGuard } from "./guards/auth-check";
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
@ -9,6 +10,7 @@ const router = createRouter({
scrollBehavior: () => ({ left: 0, top: 0 }), scrollBehavior: () => ({ left: 0, top: 0 }),
}); });
setupAuthCheckGuard(router);
setupPermissionGuard(router); setupPermissionGuard(router);
setupCheckStatesGuard(router); setupCheckStatesGuard(router);

View File

@ -48,6 +48,7 @@ axiosInstance.interceptors.response.use(
}, },
async (error) => { async (error) => {
if (error.response.status === 401) { if (error.response.status === 401) {
localStorage.removeItem("logged_in");
router.push({ router.push({
name: "Login", name: "Login",
}); });