Initial commit: Plane
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Some checks failed
Branch Build CE / Build Setup (push) Has been cancelled
Branch Build CE / Build-Push Admin Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Web Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Space Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Live Collaboration Docker Image (push) Has been cancelled
Branch Build CE / Build-Push API Server Docker Image (push) Has been cancelled
Branch Build CE / Build-Push Proxy Docker Image (push) Has been cancelled
Branch Build CE / Build-Push AIO Docker Image (push) Has been cancelled
Branch Build CE / Upload Build Assets (push) Has been cancelled
Branch Build CE / Build Release (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Codespell / Check for spelling errors (push) Has been cancelled
Sync Repositories / sync_changes (push) Has been cancelled
Synced from upstream: 8853637e981ed7d8a6cff32bd98e7afe20f54362
This commit is contained in:
46
apps/web/core/store/user/account.store.ts
Normal file
46
apps/web/core/store/user/account.store.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { set } from "lodash-es";
|
||||
import { makeObservable, observable } from "mobx";
|
||||
// types
|
||||
import type { IUserAccount } from "@plane/types";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
// store
|
||||
import type { CoreRootStore } from "../root.store";
|
||||
|
||||
export interface IAccountStore {
|
||||
// observables
|
||||
isLoading: boolean;
|
||||
error: any | undefined;
|
||||
// model observables
|
||||
provider_account_id: string | undefined;
|
||||
provider: string | undefined;
|
||||
}
|
||||
|
||||
export class AccountStore implements IAccountStore {
|
||||
isLoading: boolean = false;
|
||||
error: any | undefined = undefined;
|
||||
// model observables
|
||||
provider_account_id: string | undefined = undefined;
|
||||
provider: string | undefined = undefined;
|
||||
// service
|
||||
userService: UserService;
|
||||
constructor(
|
||||
private store: CoreRootStore,
|
||||
private _account: IUserAccount
|
||||
) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
error: observable,
|
||||
// model observables
|
||||
provider_account_id: observable.ref,
|
||||
provider: observable.ref,
|
||||
});
|
||||
// service
|
||||
this.userService = new UserService();
|
||||
// set account data
|
||||
Object.entries(this._account).forEach(([key, value]) => {
|
||||
set(this, [key], value ?? undefined);
|
||||
});
|
||||
}
|
||||
}
|
||||
338
apps/web/core/store/user/base-permissions.store.ts
Normal file
338
apps/web/core/store/user/base-permissions.store.ts
Normal file
@@ -0,0 +1,338 @@
|
||||
import { unset, set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
import { computedFn } from "mobx-utils";
|
||||
// plane imports
|
||||
import type { TUserPermissions, TUserPermissionsLevel } from "@plane/constants";
|
||||
import {
|
||||
EUserPermissions,
|
||||
EUserPermissionsLevel,
|
||||
WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS,
|
||||
} from "@plane/constants";
|
||||
import type { EUserProjectRoles, IUserProjectsRole, IWorkspaceMemberMe, TProjectMembership } from "@plane/types";
|
||||
import { EUserWorkspaceRoles } from "@plane/types";
|
||||
// plane web imports
|
||||
import { WorkspaceService } from "@/plane-web/services";
|
||||
import type { RootStore } from "@/plane-web/store/root.store";
|
||||
// services
|
||||
import projectMemberService from "@/services/project/project-member.service";
|
||||
import userService from "@/services/user.service";
|
||||
|
||||
// derived services
|
||||
const workspaceService = new WorkspaceService();
|
||||
|
||||
type ETempUserRole = TUserPermissions | EUserWorkspaceRoles | EUserProjectRoles; // TODO: Remove this once we have migrated user permissions to enums to plane constants package
|
||||
|
||||
export interface IBaseUserPermissionStore {
|
||||
loader: boolean;
|
||||
// observables
|
||||
workspaceUserInfo: Record<string, IWorkspaceMemberMe>; // workspaceSlug -> IWorkspaceMemberMe
|
||||
projectUserInfo: Record<string, Record<string, TProjectMembership>>; // workspaceSlug -> projectId -> TProjectMembership
|
||||
workspaceProjectsPermissions: Record<string, IUserProjectsRole>; // workspaceSlug -> IUserProjectsRole
|
||||
// computed helpers
|
||||
workspaceInfoBySlug: (workspaceSlug: string) => IWorkspaceMemberMe | undefined;
|
||||
getWorkspaceRoleByWorkspaceSlug: (workspaceSlug: string) => TUserPermissions | EUserWorkspaceRoles | undefined;
|
||||
getProjectRolesByWorkspaceSlug: (workspaceSlug: string) => IUserProjectsRole;
|
||||
getProjectRoleByWorkspaceSlugAndProjectId: (
|
||||
workspaceSlug: string,
|
||||
projectId?: string
|
||||
) => EUserPermissions | undefined;
|
||||
allowPermissions: (
|
||||
allowPermissions: ETempUserRole[],
|
||||
level: TUserPermissionsLevel,
|
||||
workspaceSlug?: string,
|
||||
projectId?: string,
|
||||
onPermissionAllowed?: () => boolean
|
||||
) => boolean;
|
||||
// actions
|
||||
fetchUserWorkspaceInfo: (workspaceSlug: string) => Promise<IWorkspaceMemberMe>;
|
||||
leaveWorkspace: (workspaceSlug: string) => Promise<void>;
|
||||
fetchUserProjectInfo: (workspaceSlug: string, projectId: string) => Promise<TProjectMembership>;
|
||||
fetchUserProjectPermissions: (workspaceSlug: string) => Promise<IUserProjectsRole>;
|
||||
joinProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
leaveProject: (workspaceSlug: string, projectId: string) => Promise<void>;
|
||||
hasPageAccess: (workspaceSlug: string, key: string) => boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* @description This store is used to handle permission layer for the currently logged user.
|
||||
* It manages workspace and project level permissions, roles and access control.
|
||||
*/
|
||||
export abstract class BaseUserPermissionStore implements IBaseUserPermissionStore {
|
||||
loader: boolean = false;
|
||||
// constants
|
||||
workspaceUserInfo: Record<string, IWorkspaceMemberMe> = {};
|
||||
projectUserInfo: Record<string, Record<string, TProjectMembership>> = {};
|
||||
workspaceProjectsPermissions: Record<string, IUserProjectsRole> = {};
|
||||
// observables
|
||||
|
||||
constructor(protected store: RootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
loader: observable.ref,
|
||||
workspaceUserInfo: observable,
|
||||
projectUserInfo: observable,
|
||||
workspaceProjectsPermissions: observable,
|
||||
// computed
|
||||
// actions
|
||||
fetchUserWorkspaceInfo: action,
|
||||
leaveWorkspace: action,
|
||||
fetchUserProjectInfo: action,
|
||||
fetchUserProjectPermissions: action,
|
||||
joinProject: action,
|
||||
leaveProject: action,
|
||||
});
|
||||
}
|
||||
|
||||
// computed helpers
|
||||
/**
|
||||
* @description Returns the current workspace information
|
||||
* @param { string } workspaceSlug
|
||||
* @returns { IWorkspaceMemberMe | undefined }
|
||||
*/
|
||||
workspaceInfoBySlug = computedFn((workspaceSlug: string): IWorkspaceMemberMe | undefined => {
|
||||
if (!workspaceSlug) return undefined;
|
||||
return this.workspaceUserInfo[workspaceSlug] || undefined;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Returns the workspace role by slug
|
||||
* @param { string } workspaceSlug
|
||||
* @returns { TUserPermissions | EUserWorkspaceRoles | undefined }
|
||||
*/
|
||||
getWorkspaceRoleByWorkspaceSlug = computedFn(
|
||||
(workspaceSlug: string): TUserPermissions | EUserWorkspaceRoles | undefined => {
|
||||
if (!workspaceSlug) return undefined;
|
||||
return this.workspaceUserInfo[workspaceSlug]?.role as TUserPermissions | EUserWorkspaceRoles | undefined;
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* @description Returns the project membership permission
|
||||
* @param { string } workspaceSlug
|
||||
* @param { string } projectId
|
||||
* @returns { EUserPermissions | undefined }
|
||||
*/
|
||||
protected getProjectRole = computedFn((workspaceSlug: string, projectId: string): EUserPermissions | undefined => {
|
||||
if (!workspaceSlug || !projectId) return undefined;
|
||||
const projectRole = this.workspaceProjectsPermissions?.[workspaceSlug]?.[projectId];
|
||||
if (!projectRole) return undefined;
|
||||
const workspaceRole = this.workspaceUserInfo?.[workspaceSlug]?.role;
|
||||
if (workspaceRole === EUserWorkspaceRoles.ADMIN) return EUserPermissions.ADMIN;
|
||||
else return projectRole;
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Returns the project permissions by workspace slug
|
||||
* @param { string } workspaceSlug
|
||||
* @returns { IUserProjectsRole }
|
||||
*/
|
||||
getProjectRolesByWorkspaceSlug = computedFn((workspaceSlug: string): IUserProjectsRole => {
|
||||
const projectPermissions = this.workspaceProjectsPermissions[workspaceSlug] || {};
|
||||
return Object.keys(projectPermissions).reduce((acc, projectId) => {
|
||||
const projectRole = this.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId);
|
||||
if (projectRole) {
|
||||
acc[projectId] = projectRole;
|
||||
}
|
||||
return acc;
|
||||
}, {} as IUserProjectsRole);
|
||||
});
|
||||
|
||||
/**
|
||||
* @description Returns the current project permissions
|
||||
* @param { string } workspaceSlug
|
||||
* @param { string } projectId
|
||||
* @returns { EUserPermissions | undefined }
|
||||
*/
|
||||
abstract getProjectRoleByWorkspaceSlugAndProjectId: (
|
||||
workspaceSlug: string,
|
||||
projectId?: string
|
||||
) => EUserPermissions | undefined;
|
||||
|
||||
/**
|
||||
* @description Returns whether the user has the permission to access a page
|
||||
* @param { string } page
|
||||
* @returns { boolean }
|
||||
*/
|
||||
hasPageAccess = computedFn((workspaceSlug: string, key: string): boolean => {
|
||||
if (!workspaceSlug || !key) return false;
|
||||
const settings = WORKSPACE_SIDEBAR_DYNAMIC_NAVIGATION_ITEMS_LINKS.find((item) => item.key === key);
|
||||
if (settings) {
|
||||
return this.allowPermissions(settings.access, EUserPermissionsLevel.WORKSPACE, workspaceSlug);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// action helpers
|
||||
/**
|
||||
* @description Returns whether the user has the permission to perform an action
|
||||
* @param { TUserPermissions[] } allowPermissions
|
||||
* @param { TUserPermissionsLevel } level
|
||||
* @param { string } workspaceSlug
|
||||
* @param { string } projectId
|
||||
* @param { () => boolean } onPermissionAllowed
|
||||
* @returns { boolean }
|
||||
*/
|
||||
allowPermissions = (
|
||||
allowPermissions: ETempUserRole[],
|
||||
level: TUserPermissionsLevel,
|
||||
workspaceSlug?: string,
|
||||
projectId?: string,
|
||||
onPermissionAllowed?: () => boolean
|
||||
): boolean => {
|
||||
const { workspaceSlug: currentWorkspaceSlug, projectId: currentProjectId } = this.store.router;
|
||||
if (!workspaceSlug) workspaceSlug = currentWorkspaceSlug;
|
||||
if (!projectId) projectId = currentProjectId;
|
||||
|
||||
let currentUserRole: TUserPermissions | undefined = undefined;
|
||||
|
||||
if (level === EUserPermissionsLevel.WORKSPACE) {
|
||||
currentUserRole = (workspaceSlug && this.getWorkspaceRoleByWorkspaceSlug(workspaceSlug)) as
|
||||
| EUserPermissions
|
||||
| undefined;
|
||||
}
|
||||
|
||||
if (level === EUserPermissionsLevel.PROJECT) {
|
||||
currentUserRole = (workspaceSlug &&
|
||||
projectId &&
|
||||
this.getProjectRoleByWorkspaceSlugAndProjectId(workspaceSlug, projectId)) as EUserPermissions | undefined;
|
||||
}
|
||||
|
||||
if (typeof currentUserRole === "string") {
|
||||
currentUserRole = parseInt(currentUserRole);
|
||||
}
|
||||
|
||||
if (currentUserRole && typeof currentUserRole === "number" && allowPermissions.includes(currentUserRole)) {
|
||||
if (onPermissionAllowed) {
|
||||
return onPermissionAllowed();
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description Fetches the user's workspace information
|
||||
* @param { string } workspaceSlug
|
||||
* @returns { Promise<IWorkspaceMemberMe | undefined> }
|
||||
*/
|
||||
fetchUserWorkspaceInfo = async (workspaceSlug: string): Promise<IWorkspaceMemberMe> => {
|
||||
try {
|
||||
this.loader = true;
|
||||
const response = await workspaceService.workspaceMemberMe(workspaceSlug);
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
set(this.workspaceUserInfo, [workspaceSlug], response);
|
||||
this.loader = false;
|
||||
});
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error fetching user workspace information", error);
|
||||
this.loader = false;
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Leaves a workspace
|
||||
* @param { string } workspaceSlug
|
||||
* @returns { Promise<void | undefined> }
|
||||
*/
|
||||
leaveWorkspace = async (workspaceSlug: string): Promise<void> => {
|
||||
try {
|
||||
await userService.leaveWorkspace(workspaceSlug);
|
||||
runInAction(() => {
|
||||
unset(this.workspaceUserInfo, workspaceSlug);
|
||||
unset(this.projectUserInfo, workspaceSlug);
|
||||
unset(this.workspaceProjectsPermissions, workspaceSlug);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error user leaving the workspace", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Fetches the user's project information
|
||||
* @param { string } workspaceSlug
|
||||
* @param { string } projectId
|
||||
* @returns { Promise<TProjectMembership | undefined> }
|
||||
*/
|
||||
fetchUserProjectInfo = async (workspaceSlug: string, projectId: string): Promise<TProjectMembership> => {
|
||||
try {
|
||||
const response = await projectMemberService.projectMemberMe(workspaceSlug, projectId);
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
set(this.projectUserInfo, [workspaceSlug, projectId], response);
|
||||
set(this.workspaceProjectsPermissions, [workspaceSlug, projectId], response.role);
|
||||
});
|
||||
}
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error fetching user project information", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Fetches the user's project permissions
|
||||
* @param { string } workspaceSlug
|
||||
* @returns { Promise<IUserProjectsRole | undefined> }
|
||||
*/
|
||||
fetchUserProjectPermissions = async (workspaceSlug: string): Promise<IUserProjectsRole> => {
|
||||
try {
|
||||
const response = await workspaceService.getWorkspaceUserProjectsRole(workspaceSlug);
|
||||
runInAction(() => {
|
||||
set(this.workspaceProjectsPermissions, [workspaceSlug], response);
|
||||
});
|
||||
return response;
|
||||
} catch (error) {
|
||||
console.error("Error fetching user project permissions", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Joins a project
|
||||
* @param { string } workspaceSlug
|
||||
* @param { string } projectId
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
joinProject = async (workspaceSlug: string, projectId: string): Promise<void> => {
|
||||
try {
|
||||
const response = await userService.joinProject(workspaceSlug, [projectId]);
|
||||
const projectMemberRole = this.getWorkspaceRoleByWorkspaceSlug(workspaceSlug) ?? EUserPermissions.MEMBER;
|
||||
if (response) {
|
||||
runInAction(() => {
|
||||
set(this.workspaceProjectsPermissions, [workspaceSlug, projectId], projectMemberRole);
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error user joining the project", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description Leaves a project
|
||||
* @param { string } workspaceSlug
|
||||
* @param { string } projectId
|
||||
* @returns { Promise<void> }
|
||||
*/
|
||||
leaveProject = async (workspaceSlug: string, projectId: string): Promise<void> => {
|
||||
try {
|
||||
await userService.leaveProject(workspaceSlug, projectId);
|
||||
runInAction(() => {
|
||||
unset(this.workspaceProjectsPermissions, [workspaceSlug, projectId]);
|
||||
unset(this.projectUserInfo, [workspaceSlug, projectId]);
|
||||
unset(this.store.projectRoot.project.projectMap, [projectId]);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error user leaving the project", error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
303
apps/web/core/store/user/index.ts
Normal file
303
apps/web/core/store/user/index.ts
Normal file
@@ -0,0 +1,303 @@
|
||||
import { cloneDeep, set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction, computed } from "mobx";
|
||||
// plane imports
|
||||
import { EUserPermissions, API_BASE_URL } from "@plane/constants";
|
||||
import type { IUser, TUserPermissions } from "@plane/types";
|
||||
// local
|
||||
import { persistence } from "@/local-db/storage.sqlite";
|
||||
// plane web imports
|
||||
import type { RootStore } from "@/plane-web/store/root.store";
|
||||
import type { IUserPermissionStore } from "@/plane-web/store/user/permission.store";
|
||||
import { UserPermissionStore } from "@/plane-web/store/user/permission.store";
|
||||
// services
|
||||
import { AuthService } from "@/services/auth.service";
|
||||
import { UserService } from "@/services/user.service";
|
||||
// stores
|
||||
import type { IAccountStore } from "@/store/user/account.store";
|
||||
import type { IUserProfileStore } from "@/store/user/profile.store";
|
||||
import { ProfileStore } from "@/store/user/profile.store";
|
||||
// local imports
|
||||
import type { IUserSettingsStore } from "./settings.store";
|
||||
import { UserSettingsStore } from "./settings.store";
|
||||
|
||||
type TUserErrorStatus = {
|
||||
status: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface IUserStore {
|
||||
// observables
|
||||
isAuthenticated: boolean;
|
||||
isLoading: boolean;
|
||||
error: TUserErrorStatus | undefined;
|
||||
data: IUser | undefined;
|
||||
// store observables
|
||||
userProfile: IUserProfileStore;
|
||||
userSettings: IUserSettingsStore;
|
||||
accounts: Record<string, IAccountStore>;
|
||||
permission: IUserPermissionStore;
|
||||
// actions
|
||||
fetchCurrentUser: () => Promise<IUser | undefined>;
|
||||
updateCurrentUser: (data: Partial<IUser>) => Promise<IUser | undefined>;
|
||||
handleSetPassword: (csrfToken: string, data: { password: string }) => Promise<IUser | undefined>;
|
||||
deactivateAccount: () => Promise<void>;
|
||||
changePassword: (
|
||||
csrfToken: string,
|
||||
payload: { old_password?: string; new_password: string }
|
||||
) => Promise<IUser | undefined>;
|
||||
reset: () => void;
|
||||
signOut: () => Promise<void>;
|
||||
// computed
|
||||
localDBEnabled: boolean;
|
||||
canPerformAnyCreateAction: boolean;
|
||||
projectsWithCreatePermissions: { [projectId: string]: number } | null;
|
||||
}
|
||||
|
||||
export class UserStore implements IUserStore {
|
||||
// observables
|
||||
isAuthenticated: boolean = false;
|
||||
isLoading: boolean = false;
|
||||
error: TUserErrorStatus | undefined = undefined;
|
||||
data: IUser | undefined = undefined;
|
||||
// store observables
|
||||
userProfile: IUserProfileStore;
|
||||
userSettings: IUserSettingsStore;
|
||||
accounts: Record<string, IAccountStore> = {};
|
||||
permission: IUserPermissionStore;
|
||||
// service
|
||||
userService: UserService;
|
||||
authService: AuthService;
|
||||
|
||||
constructor(private store: RootStore) {
|
||||
// stores
|
||||
this.userProfile = new ProfileStore(store);
|
||||
this.userSettings = new UserSettingsStore();
|
||||
this.permission = new UserPermissionStore(store);
|
||||
// service
|
||||
this.userService = new UserService();
|
||||
this.authService = new AuthService();
|
||||
// observables
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isAuthenticated: observable.ref,
|
||||
isLoading: observable.ref,
|
||||
error: observable,
|
||||
// model observables
|
||||
data: observable,
|
||||
userProfile: observable,
|
||||
userSettings: observable,
|
||||
accounts: observable,
|
||||
permission: observable,
|
||||
// actions
|
||||
fetchCurrentUser: action,
|
||||
updateCurrentUser: action,
|
||||
handleSetPassword: action,
|
||||
deactivateAccount: action,
|
||||
changePassword: action,
|
||||
reset: action,
|
||||
signOut: action,
|
||||
// computed
|
||||
canPerformAnyCreateAction: computed,
|
||||
projectsWithCreatePermissions: computed,
|
||||
|
||||
localDBEnabled: computed,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description fetches the current user
|
||||
* @returns {Promise<IUser>}
|
||||
*/
|
||||
fetchCurrentUser = async (): Promise<IUser> => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
});
|
||||
const user = await this.userService.currentUser();
|
||||
if (user && user?.id) {
|
||||
await Promise.all([
|
||||
this.userProfile.fetchUserProfile(),
|
||||
this.userSettings.fetchCurrentUserSettings(),
|
||||
this.store.workspaceRoot.fetchWorkspaces(),
|
||||
]);
|
||||
runInAction(() => {
|
||||
this.data = user;
|
||||
this.isLoading = false;
|
||||
this.isAuthenticated = true;
|
||||
});
|
||||
} else
|
||||
runInAction(() => {
|
||||
this.data = user;
|
||||
this.isLoading = false;
|
||||
this.isAuthenticated = false;
|
||||
});
|
||||
return user;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.isAuthenticated = false;
|
||||
this.error = {
|
||||
status: "user-fetch-error",
|
||||
message: "Failed to fetch current user",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updates the current user
|
||||
* @param data
|
||||
* @returns {Promise<IUser>}
|
||||
*/
|
||||
updateCurrentUser = async (data: Partial<IUser>): Promise<IUser> => {
|
||||
const currentUserData = this.data;
|
||||
try {
|
||||
if (currentUserData) {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof IUser = key as keyof IUser;
|
||||
if (this.data) set(this.data, userKey, data[userKey]);
|
||||
});
|
||||
}
|
||||
const user = await this.userService.updateUser(data);
|
||||
return user;
|
||||
} catch (error) {
|
||||
if (currentUserData) {
|
||||
Object.keys(currentUserData).forEach((key: string) => {
|
||||
const userKey: keyof IUser = key as keyof IUser;
|
||||
if (this.data) set(this.data, userKey, currentUserData[userKey]);
|
||||
});
|
||||
}
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
status: "user-update-error",
|
||||
message: "Failed to update current user",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description update the user password
|
||||
* @param data
|
||||
* @returns {Promise<IUser>}
|
||||
*/
|
||||
handleSetPassword = async (csrfToken: string, data: { password: string }): Promise<IUser | undefined> => {
|
||||
const currentUserData = cloneDeep(this.data);
|
||||
try {
|
||||
if (currentUserData && currentUserData.is_password_autoset && this.data) {
|
||||
const user = await this.authService.setPassword(csrfToken, { password: data.password });
|
||||
set(this.data, ["is_password_autoset"], false);
|
||||
return user;
|
||||
}
|
||||
return undefined;
|
||||
} catch (error) {
|
||||
if (this.data) set(this.data, ["is_password_autoset"], true);
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
status: "user-update-error",
|
||||
message: "Failed to update current user",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
changePassword = async (
|
||||
csrfToken: string,
|
||||
payload: {
|
||||
old_password?: string;
|
||||
new_password: string;
|
||||
}
|
||||
): Promise<IUser | undefined> => {
|
||||
try {
|
||||
const user = await this.userService.changePassword(csrfToken, payload);
|
||||
if (this.data) set(this.data, ["is_password_autoset"], false);
|
||||
return user;
|
||||
} catch (error) {
|
||||
console.log(error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description deactivates the current user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
deactivateAccount = async (): Promise<void> => {
|
||||
await this.userService.deactivateAccount();
|
||||
this.store.resetOnSignOut();
|
||||
};
|
||||
|
||||
/**
|
||||
* @description resets the user store
|
||||
* @returns {void}
|
||||
*/
|
||||
reset = (): void => {
|
||||
runInAction(() => {
|
||||
this.isAuthenticated = false;
|
||||
this.isLoading = false;
|
||||
this.error = undefined;
|
||||
this.data = undefined;
|
||||
this.userProfile = new ProfileStore(this.store);
|
||||
this.userSettings = new UserSettingsStore();
|
||||
this.permission = new UserPermissionStore(this.store);
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @description signs out the current user
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
signOut = async (): Promise<void> => {
|
||||
await this.authService.signOut(API_BASE_URL);
|
||||
await persistence.clearStorage(true);
|
||||
this.store.resetOnSignOut();
|
||||
};
|
||||
|
||||
// helper actions
|
||||
/**
|
||||
* @description fetches the projects with write permissions
|
||||
* @returns {{[projectId: string]: number} || null}
|
||||
*/
|
||||
fetchProjectsWithCreatePermissions = (): { [key: string]: TUserPermissions } => {
|
||||
const { workspaceSlug } = this.store.router;
|
||||
|
||||
const allWorkspaceProjectRoles = this.permission.getProjectRolesByWorkspaceSlug(workspaceSlug || "");
|
||||
|
||||
const userPermissions =
|
||||
(allWorkspaceProjectRoles &&
|
||||
Object.keys(allWorkspaceProjectRoles)
|
||||
.filter((key) => allWorkspaceProjectRoles[key] >= EUserPermissions.MEMBER)
|
||||
.reduce(
|
||||
(res: { [projectId: string]: number }, key: string) => ((res[key] = allWorkspaceProjectRoles[key]), res),
|
||||
{}
|
||||
)) ||
|
||||
null;
|
||||
|
||||
return userPermissions;
|
||||
};
|
||||
|
||||
/**
|
||||
* @description returns projects where user has permissions
|
||||
* @returns {{[projectId: string]: number} || null}
|
||||
*/
|
||||
get projectsWithCreatePermissions() {
|
||||
return this.fetchProjectsWithCreatePermissions();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description returns true if user has permissions to write in any project
|
||||
* @returns {boolean}
|
||||
*/
|
||||
get canPerformAnyCreateAction() {
|
||||
const filteredProjects = this.fetchProjectsWithCreatePermissions();
|
||||
return filteredProjects ? Object.keys(filteredProjects).length > 0 : false;
|
||||
}
|
||||
|
||||
get localDBEnabled() {
|
||||
return this.userSettings.canUseLocalDB;
|
||||
}
|
||||
}
|
||||
243
apps/web/core/store/user/profile.store.ts
Normal file
243
apps/web/core/store/user/profile.store.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { cloneDeep, set } from "lodash-es";
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// types
|
||||
import type { IUserTheme, TUserProfile } from "@plane/types";
|
||||
import { EStartOfTheWeek } from "@plane/types";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
// store
|
||||
import type { CoreRootStore } from "../root.store";
|
||||
|
||||
type TError = {
|
||||
status: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface IUserProfileStore {
|
||||
// observables
|
||||
isLoading: boolean;
|
||||
error: TError | undefined;
|
||||
data: TUserProfile;
|
||||
// actions
|
||||
fetchUserProfile: () => Promise<TUserProfile | undefined>;
|
||||
updateUserProfile: (data: Partial<TUserProfile>) => Promise<TUserProfile | undefined>;
|
||||
finishUserOnboarding: () => Promise<void>;
|
||||
updateTourCompleted: () => Promise<TUserProfile | undefined>;
|
||||
updateUserTheme: (data: Partial<IUserTheme>) => Promise<TUserProfile | undefined>;
|
||||
}
|
||||
|
||||
export class ProfileStore implements IUserProfileStore {
|
||||
isLoading: boolean = false;
|
||||
error: TError | undefined = undefined;
|
||||
data: TUserProfile = {
|
||||
id: undefined,
|
||||
user: undefined,
|
||||
role: undefined,
|
||||
last_workspace_id: undefined,
|
||||
theme: {
|
||||
theme: undefined,
|
||||
text: undefined,
|
||||
palette: undefined,
|
||||
primary: undefined,
|
||||
background: undefined,
|
||||
darkPalette: undefined,
|
||||
sidebarText: undefined,
|
||||
sidebarBackground: undefined,
|
||||
},
|
||||
onboarding_step: {
|
||||
workspace_join: false,
|
||||
profile_complete: false,
|
||||
workspace_create: false,
|
||||
workspace_invite: false,
|
||||
},
|
||||
is_onboarded: false,
|
||||
is_tour_completed: false,
|
||||
use_case: undefined,
|
||||
billing_address_country: undefined,
|
||||
billing_address: undefined,
|
||||
has_billing_address: false,
|
||||
has_marketing_email_consent: false,
|
||||
created_at: "",
|
||||
updated_at: "",
|
||||
language: "",
|
||||
start_of_the_week: EStartOfTheWeek.SUNDAY,
|
||||
};
|
||||
|
||||
// services
|
||||
userService: UserService;
|
||||
|
||||
constructor(public store: CoreRootStore) {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
error: observable,
|
||||
data: observable,
|
||||
// actions
|
||||
fetchUserProfile: action,
|
||||
updateUserProfile: action,
|
||||
updateTourCompleted: action,
|
||||
updateUserTheme: action,
|
||||
});
|
||||
// services
|
||||
this.userService = new UserService();
|
||||
}
|
||||
|
||||
// helper action
|
||||
mutateUserProfile = (data: Partial<TUserProfile>) => {
|
||||
if (!data) return;
|
||||
Object.entries(data).forEach(([key, value]) => {
|
||||
if (key in this.data) set(this.data, key, value);
|
||||
});
|
||||
};
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description fetches user profile information
|
||||
* @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
fetchUserProfile = async (): Promise<TUserProfile | undefined> => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
});
|
||||
const userProfile = await this.userService.getCurrentUserProfile();
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.data = userProfile;
|
||||
});
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.error = {
|
||||
status: "user-profile-fetch-error",
|
||||
message: "Failed to fetch user profile",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updated the user profile information
|
||||
* @param {Partial<TUserProfile>} data
|
||||
* @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
updateUserProfile = async (data: Partial<TUserProfile>): Promise<TUserProfile | undefined> => {
|
||||
const currentUserProfileData = this.data;
|
||||
try {
|
||||
if (currentUserProfileData) {
|
||||
this.mutateUserProfile(data);
|
||||
}
|
||||
const userProfile = await this.userService.updateCurrentUserProfile(data);
|
||||
return userProfile;
|
||||
} catch {
|
||||
if (currentUserProfileData) {
|
||||
this.mutateUserProfile(currentUserProfileData);
|
||||
}
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
status: "user-profile-update-error",
|
||||
message: "Failed to update user profile",
|
||||
};
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description finishes the user onboarding
|
||||
* @returns { void }
|
||||
*/
|
||||
finishUserOnboarding = async (): Promise<void> => {
|
||||
try {
|
||||
const firstWorkspace = Object.values(this.store.workspaceRoot.workspaces ?? {})?.[0];
|
||||
const dataToUpdate: Partial<TUserProfile> = {
|
||||
onboarding_step: {
|
||||
profile_complete: true,
|
||||
workspace_join: true,
|
||||
workspace_create: true,
|
||||
workspace_invite: true,
|
||||
},
|
||||
last_workspace_id: firstWorkspace?.id,
|
||||
};
|
||||
|
||||
// update user onboarding steps
|
||||
await this.userService.updateCurrentUserProfile(dataToUpdate);
|
||||
|
||||
// update user onboarding status
|
||||
await this.userService.updateUserOnBoard();
|
||||
|
||||
// Wait for user settings to be refreshed with cache-busting before updating onboarding status
|
||||
await Promise.all([
|
||||
this.fetchUserProfile(),
|
||||
this.store.user.userSettings.fetchCurrentUserSettings(true), // Cache-busting enabled
|
||||
]);
|
||||
|
||||
// Only after settings are refreshed, update the user profile store to mark as onboarded
|
||||
runInAction(() => {
|
||||
this.mutateUserProfile({ ...dataToUpdate, is_onboarded: true });
|
||||
});
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.error = {
|
||||
status: "user-profile-onboard-finish-error",
|
||||
message: "Failed to finish user onboarding",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updates the user tour completed status
|
||||
* @returns @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
updateTourCompleted = async () => {
|
||||
const isUserProfileTourCompleted = this.data.is_tour_completed || false;
|
||||
try {
|
||||
this.mutateUserProfile({ is_tour_completed: true });
|
||||
const userProfile = await this.userService.updateUserTourCompleted();
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.mutateUserProfile({ is_tour_completed: isUserProfileTourCompleted });
|
||||
this.error = {
|
||||
status: "user-profile-tour-complete-error",
|
||||
message: "Failed to update user profile is_tour_completed",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* @description updates the user theme
|
||||
* @returns @returns {Promise<TUserProfile | undefined>}
|
||||
*/
|
||||
updateUserTheme = async (data: Partial<IUserTheme>) => {
|
||||
const currentProfileTheme = cloneDeep(this.data.theme);
|
||||
try {
|
||||
runInAction(() => {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof IUserTheme = key as keyof IUserTheme;
|
||||
if (this.data.theme) set(this.data.theme, userKey, data[userKey]);
|
||||
});
|
||||
});
|
||||
const userProfile = await this.userService.updateCurrentUserProfile({ theme: this.data.theme });
|
||||
return userProfile;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
Object.keys(data).forEach((key: string) => {
|
||||
const userKey: keyof IUserTheme = key as keyof IUserTheme;
|
||||
if (currentProfileTheme) set(this.data.theme, userKey, currentProfileTheme[userKey]);
|
||||
});
|
||||
this.error = {
|
||||
status: "user-profile-theme-update-error",
|
||||
message: "Failed to update user profile theme",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
104
apps/web/core/store/user/settings.store.ts
Normal file
104
apps/web/core/store/user/settings.store.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
import { action, makeObservable, observable, runInAction } from "mobx";
|
||||
// plane imports
|
||||
import type { IUserSettings } from "@plane/types";
|
||||
// services
|
||||
import { UserService } from "@/services/user.service";
|
||||
|
||||
type TError = {
|
||||
status: string;
|
||||
message: string;
|
||||
};
|
||||
|
||||
export interface IUserSettingsStore {
|
||||
// observables
|
||||
isLoading: boolean;
|
||||
error: TError | undefined;
|
||||
data: IUserSettings;
|
||||
canUseLocalDB: boolean;
|
||||
sidebarCollapsed: boolean;
|
||||
isScrolled: boolean;
|
||||
// actions
|
||||
fetchCurrentUserSettings: (bustCache?: boolean) => Promise<IUserSettings | undefined>;
|
||||
toggleSidebar: (collapsed?: boolean) => void;
|
||||
toggleIsScrolled: (isScrolled?: boolean) => void;
|
||||
}
|
||||
|
||||
export class UserSettingsStore implements IUserSettingsStore {
|
||||
// observables
|
||||
isLoading: boolean = false;
|
||||
sidebarCollapsed: boolean = true;
|
||||
error: TError | undefined = undefined;
|
||||
isScrolled: boolean = false;
|
||||
data: IUserSettings = {
|
||||
id: undefined,
|
||||
email: undefined,
|
||||
workspace: {
|
||||
last_workspace_id: undefined,
|
||||
last_workspace_slug: undefined,
|
||||
last_workspace_name: undefined,
|
||||
last_workspace_logo: undefined,
|
||||
fallback_workspace_id: undefined,
|
||||
fallback_workspace_slug: undefined,
|
||||
invites: undefined,
|
||||
},
|
||||
};
|
||||
canUseLocalDB: boolean = false;
|
||||
// services
|
||||
userService: UserService;
|
||||
|
||||
constructor() {
|
||||
makeObservable(this, {
|
||||
// observables
|
||||
isLoading: observable.ref,
|
||||
error: observable,
|
||||
data: observable,
|
||||
canUseLocalDB: observable.ref,
|
||||
sidebarCollapsed: observable.ref,
|
||||
isScrolled: observable.ref,
|
||||
// actions
|
||||
fetchCurrentUserSettings: action,
|
||||
toggleSidebar: action,
|
||||
toggleIsScrolled: action,
|
||||
});
|
||||
// services
|
||||
this.userService = new UserService();
|
||||
}
|
||||
|
||||
// actions
|
||||
toggleSidebar = (collapsed?: boolean) => {
|
||||
this.sidebarCollapsed = collapsed ?? !this.sidebarCollapsed;
|
||||
};
|
||||
|
||||
toggleIsScrolled = (isScrolled?: boolean) => {
|
||||
this.isScrolled = isScrolled ?? !this.isScrolled;
|
||||
};
|
||||
|
||||
// actions
|
||||
/**
|
||||
* @description fetches user profile information
|
||||
* @returns {Promise<IUserSettings | undefined>}
|
||||
*/
|
||||
fetchCurrentUserSettings = async (bustCache: boolean = false) => {
|
||||
try {
|
||||
runInAction(() => {
|
||||
this.isLoading = true;
|
||||
this.error = undefined;
|
||||
});
|
||||
const userSettings = await this.userService.currentUserSettings(bustCache);
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.data = userSettings;
|
||||
});
|
||||
return userSettings;
|
||||
} catch (error) {
|
||||
runInAction(() => {
|
||||
this.isLoading = false;
|
||||
this.error = {
|
||||
status: "error",
|
||||
message: "Failed to fetch user settings",
|
||||
};
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user