Browse Source

refactor: managing logged-in user states with pinia (#699)

#### What type of PR is this?

/kind improvement
/milestone 2.0

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

使用 Pinia 管理已授权用户信息,并提供判断是否是 `anonymousUser` 的方式。

适配 https://github.com/halo-dev/halo/pull/2729

#### Special notes for your reviewer:

/cc @halo-dev/sig-halo-console 

测试流程:

1. Halo 需要切换到 https://github.com/halo-dev/halo/pull/2729 的分支。
2. 测试 Console 端的登录、退出等流程。

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

```release-note
None
```
pull/703/head
Ryan Wang 2 years ago committed by GitHub
parent
commit
d8805f30a0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 21
      src/layouts/BasicLayout.vue
  2. 17
      src/main.ts
  3. 8
      src/modules/dashboard/widgets/QuickLinkWidget.vue
  4. 12
      src/modules/system/users/Login.vue
  5. 16
      src/modules/system/users/UserList.vue
  6. 7
      src/modules/system/users/components/UserPasswordChangeModal.vue
  7. 6
      src/router/guards/auth-check.ts
  8. 26
      src/stores/user.ts

21
src/layouts/BasicLayout.vue

@ -11,7 +11,6 @@ import {
} from "@halo-dev/components";
import { RoutesMenu } from "@/components/menu/RoutesMenu";
import type { MenuGroupType, MenuItemType } from "@halo-dev/console-shared";
import type { User } from "@halo-dev/api-client";
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
import {
RouterView,
@ -19,13 +18,14 @@ import {
useRouter,
type RouteRecordRaw,
} from "vue-router";
import { computed, inject, onMounted, onUnmounted, ref } from "vue";
import { computed, onMounted, onUnmounted, ref } from "vue";
import axios from "axios";
import GlobalSearchModal from "@/components/global-search/GlobalSearchModal.vue";
import { coreMenuGroups } from "@/router/routes.config";
import sortBy from "lodash.sortby";
import { useRoleStore } from "@/stores/role";
import { hasPermission } from "@/utils/permission";
import { useUserStore } from "@/stores/user";
const route = useRoute();
const router = useRouter();
@ -33,7 +33,7 @@ const router = useRouter();
const moreMenuVisible = ref(false);
const moreMenuRootVisible = ref(false);
const currentUser = inject<User>("currentUser");
const userStore = useUserStore();
const handleLogout = () => {
Dialog.warning({
@ -43,6 +43,9 @@ const handleLogout = () => {
await axios.post(`${import.meta.env.VITE_API_URL}/logout`, undefined, {
withCredentials: true,
});
await userStore.fetchCurrentUser();
router.replace({ name: "Login" });
} catch (error) {
console.error("Failed to logout", error);
@ -53,7 +56,7 @@ const handleLogout = () => {
const currentRole = computed(() => {
return JSON.parse(
currentUser?.metadata.annotations?.[
userStore.currentUser?.metadata.annotations?.[
"rbac.authorization.halo.run/role-names"
] || "[]"
)[0];
@ -203,17 +206,17 @@ onMounted(generateMenus);
</div>
<RoutesMenu :menus="menus" />
<div class="current-profile">
<div v-if="currentUser?.spec.avatar" class="profile-avatar">
<div v-if="userStore.currentUser?.spec.avatar" class="profile-avatar">
<VAvatar
:src="currentUser?.spec.avatar"
:alt="currentUser?.spec.displayName"
:src="userStore.currentUser?.spec.avatar"
:alt="userStore.currentUser?.spec.displayName"
size="md"
circle
></VAvatar>
</div>
<div class="profile-name">
<div class="flex text-sm font-medium">
{{ currentUser?.spec.displayName }}
{{ userStore.currentUser?.spec.displayName }}
</div>
<div class="flex">
<VTag>
@ -237,7 +240,7 @@ onMounted(generateMenus);
type="secondary"
:route="{
name: 'UserDetail',
params: { name: currentUser?.metadata.name },
params: { name: userStore.currentUser?.metadata.name },
}"
>
个人资料

17
src/main.ts

@ -13,12 +13,12 @@ import { setupComponents } from "./setup/setupComponents";
import { coreModules } from "./modules";
import { useScriptTag } from "@vueuse/core";
import { usePluginStore } from "@/stores/plugin";
import type { User } from "@halo-dev/api-client";
import { hasPermission } from "@/utils/permission";
import { useRoleStore } from "@/stores/role";
import type { RouteRecordRaw } from "vue-router";
import { useThemeStore } from "./stores/theme";
import { useSystemStatesStore } from "./stores/system-states";
import { useUserStore } from "./stores/user";
const app = createApp(App);
@ -177,13 +177,7 @@ async function loadPluginModules() {
}
}
let currentUser: User | undefined = undefined;
async function loadCurrentUser() {
const { data: user } = await apiClient.user.getCurrentUserDetail();
app.provide<User>("currentUser", user);
currentUser = user;
async function loadUserPermissions() {
const { data: currentPermissions } = await apiClient.user.getPermissions({
name: "-",
});
@ -228,12 +222,14 @@ async function initApp() {
try {
loadCoreModules();
await loadCurrentUser();
const userStore = useUserStore();
await userStore.fetchCurrentUser();
if (!currentUser) {
if (userStore.isAnonymous) {
return;
}
await loadUserPermissions();
await loadActivatedTheme();
try {
@ -242,6 +238,7 @@ async function initApp() {
console.error("Failed to load plugins", e);
}
// load system setup state
const systemStateStore = useSystemStatesStore();
await systemStateStore.fetchSystemStates();
} catch (e) {

8
src/modules/dashboard/widgets/QuickLinkWidget.vue

@ -11,10 +11,10 @@ import {
VCard,
IconUserLine,
} from "@halo-dev/components";
import { inject, markRaw, ref, type Component } from "vue";
import { markRaw, ref, type Component } from "vue";
import { useRouter } from "vue-router";
import type { User } from "@halo-dev/api-client";
import ThemePreviewModal from "@/modules/interface/themes/components/preview/ThemePreviewModal.vue";
import { useUserStore } from "@/stores/user";
interface Action {
icon: Component;
@ -23,7 +23,7 @@ interface Action {
permissions?: string[];
}
const currentUser = inject<User>("currentUser");
const userStore = useUserStore();
const router = useRouter();
@ -36,7 +36,7 @@ const actions: Action[] = [
action: () => {
router.push({
name: "UserDetail",
params: { name: currentUser?.metadata.name },
params: { name: userStore.currentUser?.metadata.name },
});
},
},

12
src/modules/system/users/Login.vue

@ -6,14 +6,14 @@ import {
Toast,
} from "@halo-dev/components";
import qs from "qs";
import { inject, onBeforeMount, onMounted, ref } from "vue";
import { onBeforeMount, onMounted, ref } from "vue";
import { submitForm } from "@formkit/vue";
import router from "@/router";
import axios from "axios";
import type { User } from "@halo-dev/api-client";
import { setFocus } from "@/formkit/utils/focus";
import IconLogo from "~icons/core/logo?width=5rem&height=2rem";
import { randomUUID } from "@/utils/id";
import { useUserStore } from "@/stores/user";
interface LoginForm {
_csrf: string;
@ -21,6 +21,8 @@ interface LoginForm {
password: string;
}
const userStore = useUserStore();
const loginForm = ref<LoginForm>({
_csrf: "",
username: "",
@ -48,6 +50,9 @@ const handleLogin = async () => {
},
}
);
await userStore.fetchCurrentUser();
localStorage.setItem("logged_in", "true");
router.go(0);
} catch (e) {
@ -61,8 +66,7 @@ const handleLogin = async () => {
};
onBeforeMount(() => {
const currentUser = inject<User>("currentUser");
if (currentUser) {
if (!userStore.isAnonymous) {
router.push({ name: "Dashboard" });
}
});

16
src/modules/system/users/UserList.vue

@ -19,7 +19,7 @@ import {
import UserEditingModal from "./components/UserEditingModal.vue";
import UserPasswordChangeModal from "./components/UserPasswordChangeModal.vue";
import GrantPermissionModal from "./components/GrantPermissionModal.vue";
import { computed, inject, onMounted, ref, watch } from "vue";
import { computed, onMounted, ref, watch } from "vue";
import { apiClient } from "@/utils/api-client";
import type { User, UserList } from "@halo-dev/api-client";
import { rbacAnnotations } from "@/constants/annotations";
@ -27,6 +27,7 @@ import { formatDatetime } from "@/utils/date";
import { useRouteQuery } from "@vueuse/router";
import Fuse from "fuse.js";
import { usePermission } from "@/utils/permission";
import { useUserStore } from "@/stores/user";
const { currentUserHasPermission } = usePermission();
@ -48,7 +49,8 @@ const users = ref<UserList>({
});
const selectedUserNames = ref<string[]>([]);
const selectedUser = ref<User>();
const currentUser = inject<User>("currentUser");
const userStore = useUserStore();
let fuse: Fuse<User> | undefined = undefined;
@ -118,7 +120,7 @@ const handleDeleteInBatch = async () => {
confirmType: "danger",
onConfirm: async () => {
const userNamesToDelete = selectedUserNames.value.filter(
(name) => name != currentUser?.metadata.name
(name) => name != userStore.currentUser?.metadata.name
);
await Promise.all(
userNamesToDelete.map((name) => {
@ -464,7 +466,9 @@ onMounted(() => {
修改密码
</VButton>
<VButton
v-if="currentUser?.metadata.name !== user.metadata.name"
v-if="
userStore.currentUser?.metadata.name !== user.metadata.name
"
v-close-popper
block
@click="handleOpenGrantPermissionModal(user)"
@ -472,7 +476,9 @@ onMounted(() => {
分配角色
</VButton>
<VButton
v-if="currentUser?.metadata.name !== user.metadata.name"
v-if="
userStore.currentUser?.metadata.name !== user.metadata.name
"
v-close-popper
block
type="danger"

7
src/modules/system/users/components/UserPasswordChangeModal.vue

@ -1,12 +1,13 @@
<script lang="ts" setup>
import { VButton, VModal, VSpace } from "@halo-dev/components";
import SubmitButton from "@/components/button/SubmitButton.vue";
import { inject, ref, watch } from "vue";
import { ref, watch } from "vue";
import type { User } from "@halo-dev/api-client";
import { apiClient } from "@/utils/api-client";
import cloneDeep from "lodash.clonedeep";
import { reset } from "@formkit/core";
import { setFocus } from "@/formkit/utils/focus";
import { useUserStore } from "@/stores/user";
const props = withDefaults(
defineProps<{
@ -24,7 +25,7 @@ const emit = defineEmits<{
(event: "close"): void;
}>();
const currentUser = inject<User>("currentUser");
const userStore = useUserStore();
interface PasswordChangeFormState {
password: string;
@ -69,7 +70,7 @@ const handleChangePassword = async () => {
const changePasswordRequest = cloneDeep(formState.value);
delete changePasswordRequest.password_confirm;
if (props.user?.metadata.name === currentUser?.metadata.name) {
if (props.user?.metadata.name === userStore.currentUser?.metadata.name) {
await apiClient.user.changePassword({
name: "-",
changePasswordRequest,

6
src/router/guards/auth-check.ts

@ -1,3 +1,4 @@
import { useUserStore } from "@/stores/user";
import type { Router } from "vue-router";
export function setupAuthCheckGuard(router: Router) {
@ -6,7 +7,10 @@ export function setupAuthCheckGuard(router: Router) {
next();
return;
}
if (localStorage.getItem("logged_in") !== "true") {
const userStore = useUserStore();
if (localStorage.getItem("logged_in") !== "true" || userStore.isAnonymous) {
next({ name: "Login" });
return;
}

26
src/stores/user.ts

@ -0,0 +1,26 @@
import { apiClient } from "@/utils/api-client";
import type { User } from "@halo-dev/api-client";
import { defineStore } from "pinia";
interface UserStoreState {
currentUser?: User;
isAnonymous: boolean;
}
export const useUserStore = defineStore("user", {
state: (): UserStoreState => ({
currentUser: undefined,
isAnonymous: true,
}),
actions: {
async fetchCurrentUser() {
try {
const { data } = await apiClient.user.getCurrentUserDetail();
this.currentUser = data;
this.isAnonymous = data.metadata.name === "anonymousUser";
} catch (e) {
console.error("Failed to fetch current user", e);
}
},
},
});
Loading…
Cancel
Save