fix(editor): Remove isOwner from IUser interface (#8888)

This commit is contained in:
Csaba Tuncsik 2024-03-18 11:39:15 +01:00 committed by GitHub
parent 024be62693
commit 6955e8991c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 74 additions and 65 deletions

View file

@ -7,7 +7,8 @@ import type {
REGULAR_NODE_CREATOR_VIEW,
AI_OTHERS_NODE_CREATOR_VIEW,
VIEWS,
} from './constants';
ROLE,
} from '@/constants';
import type { IMenuItem } from 'n8n-design-system';
import {
type GenericValue,
@ -688,9 +689,9 @@ export type IPersonalizationSurveyVersions =
| IPersonalizationSurveyAnswersV2
| IPersonalizationSurveyAnswersV3;
export type IRole = 'default' | 'global:owner' | 'global:member' | 'global:admin';
export type InvitableRoleName = 'global:member' | 'global:admin';
export type Roles = typeof ROLE;
export type IRole = Roles[keyof Roles];
export type InvitableRoleName = Roles['Member' | 'Admin'];
export interface IUserResponse {
id: string;
@ -714,7 +715,6 @@ export interface IUser extends IUserResponse {
isDefaultUser: boolean;
isPendingUser: boolean;
hasRecoveryCodesLeft: boolean;
isOwner: boolean;
inviteAcceptUrl?: string;
fullName?: string;
createdAt?: string;

View file

@ -1,5 +1,6 @@
import { parsePermissionsTable } from '@/permissions';
import type { IUser } from '@/Interface';
import { ROLE } from '@/constants';
describe('parsePermissionsTable()', () => {
const user: IUser = {
@ -7,9 +8,11 @@ describe('parsePermissionsTable()', () => {
firstName: 'John',
lastName: 'Doe',
isDefaultUser: false,
isOwner: true,
isPending: false,
isPendingUser: false,
mfaEnabled: false,
hasRecoveryCodesLeft: false,
role: ROLE.Owner,
};
it('should return permissions object using generic permissions table', () => {

View file

@ -16,9 +16,6 @@ export const userFactory = Factory.extend<IUser>({
isDefaultUser() {
return false;
},
isOwner() {
return false;
},
isPending() {
return false;
},
@ -28,4 +25,10 @@ export const userFactory = Factory.extend<IUser>({
signInType(): SignInType {
return SignInType.EMAIL;
},
mfaEnabled() {
return false;
},
hasRecoveryCodesLeft() {
return false;
},
});

View file

@ -2,8 +2,8 @@ import type {
CurrentUserResponse,
IPersonalizationLatestVersion,
IRestApiContext,
IRole,
IUserResponse,
InvitableRoleName,
} from '@/Interface';
import type { IDataObject } from 'n8n-workflow';
import { makeRestApiRequest } from '@/utils/apiUtils';
@ -157,7 +157,7 @@ export async function submitPersonalizationSurvey(
export interface UpdateGlobalRolePayload {
id: string;
newRoleName: Exclude<IRole, 'default' | 'global:owner'>;
newRoleName: InvitableRoleName;
}
export async function updateGlobalRole(

View file

@ -1,5 +1,6 @@
import type { IOnboardingCallPrompt, IUser } from '@/Interface';
import { get, post } from '@/utils/apiUtils';
import { isUserGlobalOwner } from '@/utils/userUtils';
const N8N_API_BASE_URL = 'https://api.n8n.io/api';
const ONBOARDING_PROMPTS_ENDPOINT = '/prompts/onboarding';
@ -12,7 +13,7 @@ export async function fetchNextOnboardingPrompt(
return await get(N8N_API_BASE_URL, ONBOARDING_PROMPTS_ENDPOINT, {
instance_id: instanceId,
user_id: `${instanceId}#${currentUser.id}`,
is_owner: currentUser.isOwner ?? false,
is_owner: isUserGlobalOwner(currentUser),
survey_results: currentUser.personalizationAnswers,
});
}

View file

@ -66,8 +66,12 @@ import { mapStores } from 'pinia';
import { useToast } from '@/composables/useToast';
import Modal from './Modal.vue';
import type { IFormInputs, IInviteResponse, IUser } from '@/Interface';
import { ROLE } from '@/utils/userUtils';
import { EnterpriseEditionFeature, VALID_EMAIL_REGEX, INVITE_USER_MODAL_KEY } from '@/constants';
import {
EnterpriseEditionFeature,
VALID_EMAIL_REGEX,
INVITE_USER_MODAL_KEY,
ROLE,
} from '@/constants';
import { useUsersStore } from '@/stores/users.store';
import { useSettingsStore } from '@/stores/settings.store';
import { useUIStore } from '@/stores/ui.store';

View file

@ -4,6 +4,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { useCollaborationStore } from '@/stores/collaboration.store';
import { onBeforeUnmount, onMounted, computed, ref } from 'vue';
import { TIME } from '@/constants';
import { isUserGlobalOwner } from '@/utils/userUtils';
const collaborationStore = useCollaborationStore();
const usersStore = useUsersStore();
@ -16,7 +17,7 @@ const activeUsersSorted = computed(() => {
const currentWorkflowUsers = (collaborationStore.getUsersForCurrentWorkflow ?? []).map(
(userInfo) => userInfo.user,
);
const owner = currentWorkflowUsers.find((user) => user.role === 'global:owner');
const owner = currentWorkflowUsers.find(isUserGlobalOwner);
return {
defaultGroup: owner
? [owner, ...currentWorkflowUsers.filter((user) => user.id !== owner.id)]

View file

@ -2,7 +2,7 @@ import { merge } from 'lodash-es';
import userEvent from '@testing-library/user-event';
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
import { STORES } from '@/constants';
import { ROLE, STORES } from '@/constants';
import { createTestingPinia } from '@pinia/testing';
import BannerStack from '@/components/banners/BannerStack.vue';
@ -26,11 +26,11 @@ const initialState = {
users: {
'aaa-bbb': {
id: 'aaa-bbb',
role: 'global:owner',
role: ROLE.Owner,
},
'bbb-bbb': {
id: 'bbb-bbb',
role: 'global:member',
role: ROLE.Member,
},
},
},

View file

@ -1,6 +1,6 @@
import { merge } from 'lodash-es';
import { SETTINGS_STORE_DEFAULT_STATE, waitAllPromises } from '@/__tests__/utils';
import { STORES } from '@/constants';
import { ROLE, STORES } from '@/constants';
import { createTestingPinia } from '@pinia/testing';
import { useUIStore } from '@/stores/ui.store';
import CollaborationPane from '@/components//MainHeader/CollaborationPane.vue';
@ -13,10 +13,9 @@ const OWNER_USER = {
email: 'owner@user.com',
firstName: 'Owner',
lastName: 'User',
role: 'global:owner',
role: ROLE.Owner,
disabled: false,
isPending: false,
isOwner: true,
fullName: 'Owner User',
};
@ -26,10 +25,9 @@ const MEMBER_USER = {
email: 'member@user.com',
firstName: 'Member',
lastName: 'User',
role: 'global:member',
role: ROLE.Member,
disabled: false,
isPending: false,
isOwner: false,
fullName: 'Member User',
};
@ -39,10 +37,9 @@ const MEMBER_USER_2 = {
email: 'member2@user.com',
firstName: 'Another Member',
lastName: 'User',
role: 'global:member',
role: ROLE.Member,
disabled: false,
isPending: false,
isOwner: false,
fullName: 'Another Member User',
};

View file

@ -1,7 +1,7 @@
import PersonalizationModal from '@/components/PersonalizationModal.vue';
import { createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
import { PERSONALIZATION_MODAL_KEY, STORES, VIEWS } from '@/constants';
import { PERSONALIZATION_MODAL_KEY, ROLE, STORES, VIEWS } from '@/constants';
import { retry } from '@/__tests__/utils';
import { createComponentRenderer } from '@/__tests__/render';
import { fireEvent } from '@testing-library/vue';
@ -31,7 +31,7 @@ const pinia = createTestingPinia({
isDefaultUser: false,
isPendingUser: false,
hasRecoveryCodesLeft: true,
isOwner: true,
role: ROLE.Owner,
mfaEnabled: false,
},
},

View file

@ -2,6 +2,8 @@ import { render } from '@testing-library/vue';
import V1Banner from '../V1Banner.vue';
import { createPinia, setActivePinia } from 'pinia';
import { useUsersStore } from '@/stores/users.store';
import { ROLE } from '@/constants';
import type { IUser } from '@/Interface';
describe('V1 Banner', () => {
let pinia: ReturnType<typeof createPinia>;
@ -22,8 +24,8 @@ describe('V1 Banner', () => {
it('should render banner with dismiss call if user is owner', () => {
vi.spyOn(usersStore, 'currentUser', 'get').mockReturnValue({
role: 'global:owner',
});
role: ROLE.Owner,
} as IUser);
const { container } = render(V1Banner);
expect(container).toMatchSnapshot();

View file

@ -752,3 +752,10 @@ export const TEMPLATES_URLS = {
BASE_WEBSITE_URL: 'https://n8n.io/workflows',
UTM_QUERY: 'utm_source=n8n_app&utm_medium=template_library',
};
export const ROLE = {
Owner: 'global:owner',
Member: 'global:member',
Admin: 'global:admin',
Default: 'default', // default user with no email when setting up instance
} as const;

View file

@ -8,6 +8,7 @@ import type { IUser, ICredentialsResponse, IWorkflowDb } from '@/Interface';
import { EnterpriseEditionFeature, PLACEHOLDER_EMPTY_WORKFLOW_ID } from '@/constants';
import { useSettingsStore } from '@/stores/settings.store';
import { hasPermission } from './rbac/permissions';
import { isUserGlobalOwner } from './utils/userUtils';
/**
* Old permissions implementation
@ -43,7 +44,7 @@ export const parsePermissionsTable = (
table: IPermissionsTable,
): IPermissions => {
const genericTable: IPermissionsTable = [
{ name: UserRole.InstanceOwner, test: () => !!user?.isOwner },
{ name: UserRole.InstanceOwner, test: () => (user ? isUserGlobalOwner(user) : false) },
];
return [...genericTable, ...table].reduce(

View file

@ -1,6 +1,6 @@
import { useUsersStore } from '@/stores/users.store';
import { hasRole } from '@/rbac/checks';
import { ROLE } from '@/utils/userUtils';
import { ROLE } from '@/constants';
vi.mock('@/stores/users.store', () => ({
useUsersStore: vi.fn(),
@ -12,7 +12,7 @@ describe('Checks', () => {
vi.mocked(useUsersStore).mockReturnValue({
currentUser: {
isDefaultUser: false,
role: 'global:owner',
role: ROLE.Owner,
},
} as ReturnType<typeof useUsersStore>);

View file

@ -1,6 +1,6 @@
import { useUsersStore } from '@/stores/users.store';
import type { RBACPermissionCheck, RolePermissionOptions } from '@/types/rbac';
import { ROLE } from '@/utils/userUtils';
import { ROLE } from '@/constants';
import type { IRole } from '@/Interface';
export const hasRole: RBACPermissionCheck<RolePermissionOptions> = (checkRoles) => {

View file

@ -1,9 +1,8 @@
import { roleMiddleware } from '@/rbac/middleware/role';
import { useUsersStore } from '@/stores/users.store';
import { ROLE } from '@/utils/userUtils';
import type { IUser } from '@/Interface';
import type { RouteLocationNormalized } from 'vue-router';
import { VIEWS } from '@/constants';
import { VIEWS, ROLE } from '@/constants';
vi.mock('@/stores/users.store', () => ({
useUsersStore: vi.fn(),
@ -15,7 +14,7 @@ describe('Middleware', () => {
vi.mocked(useUsersStore).mockReturnValue({
currentUser: {
isDefaultUser: false,
role: 'global:owner',
role: ROLE.Owner,
} as IUser,
} as ReturnType<typeof useUsersStore>);
@ -54,7 +53,7 @@ describe('Middleware', () => {
vi.mocked(useUsersStore).mockReturnValue({
currentUser: {
isDefaultUser: false,
role: 'global:owner',
role: ROLE.Owner,
} as IUser,
} as ReturnType<typeof useUsersStore>);

View file

@ -14,6 +14,7 @@ import {
getNotTrialingUserResponse,
} from './utils/cloudStoreUtils';
import type { IRole } from '@/Interface';
import { ROLE } from '@/constants';
let uiStore: ReturnType<typeof useUIStore>;
let settingsStore: ReturnType<typeof useSettingsStore>;
@ -33,7 +34,7 @@ function setUser(role: IRole) {
}
function setupOwnerAndCloudDeployment() {
setUser('global:owner');
setUser(ROLE.Owner);
settingsStore.setSettings(
merge({}, SETTINGS_STORE_DEFAULT_STATE.settings, {
n8nMetadata: {
@ -75,19 +76,19 @@ describe('UI store', () => {
[
'default',
'production',
'global:owner',
ROLE.Owner,
'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
],
[
'default',
'development',
'global:owner',
ROLE.Owner,
'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
],
[
'cloud',
'production',
'global:owner',
ROLE.Owner,
`https://app.n8n.cloud/login?code=123&returnPath=${encodeURIComponent(
'/account/change-plan',
)}&utm_campaign=utm-test-campaign&source=test_source`,
@ -95,7 +96,7 @@ describe('UI store', () => {
[
'cloud',
'production',
'global:member',
ROLE.Member,
'https://n8n.io/pricing?utm_campaign=utm-test-campaign&source=test_source',
],
])(

View file

@ -67,11 +67,9 @@ export const useCloudPlanStore = defineStore(STORES.CLOUD_PLAN, () => {
const getUserCloudAccount = async () => {
if (!hasCloudPlan.value) throw new Error('User does not have a cloud plan');
try {
if (hasPermission(['instanceOwner'])) {
await usersStore.fetchUserCloudAccount();
if (!usersStore.currentUserCloudInfo?.confirmed && !userIsTrialing.value) {
useUIStore().pushBannerToStack('EMAIL_CONFIRMATION');
}
await usersStore.fetchUserCloudAccount();
if (!usersStore.currentUserCloudInfo?.confirmed && !userIsTrialing.value) {
useUIStore().pushBannerToStack('EMAIL_CONFIRMATION');
}
} catch (error) {
throw new Error(error.message);

View file

@ -343,7 +343,6 @@ export const useUIStore = defineStore(STORES.UI, {
let linkUrl = '';
const searchParams = new URLSearchParams();
const { isInstanceOwner } = useUsersStore();
if (deploymentType === 'cloud' && hasPermission(['instanceOwner'])) {
const adminPanelHost = new URL(window.location.href).host.split('.').slice(1).join('.');

View file

@ -18,7 +18,7 @@ import {
validateSignupToken,
updateGlobalRole,
} from '@/api/users';
import { PERSONALIZATION_MODAL_KEY, STORES } from '@/constants';
import { PERSONALIZATION_MODAL_KEY, STORES, ROLE } from '@/constants';
import type {
Cloud,
ICredentialsResponse,
@ -46,7 +46,7 @@ import type { Scope } from '@n8n/permissions';
import { inviteUsers, acceptInvitation } from '@/api/invitation';
const isPendingUser = (user: IUserResponse | null) => !!user?.isPending;
const isInstanceOwner = (user: IUserResponse | null) => user?.role === 'global:owner';
const isInstanceOwner = (user: IUserResponse | null) => user?.role === ROLE.Owner;
const isDefaultUser = (user: IUserResponse | null) => isInstanceOwner(user) && isPendingUser(user);
export const useUsersStore = defineStore(STORES.USERS, {
@ -139,7 +139,6 @@ export const useUsersStore = defineStore(STORES.USERS, {
: undefined,
isDefaultUser: isDefaultUser(updatedUser),
isPendingUser: isPendingUser(updatedUser),
isOwner: isInstanceOwner(updatedUser),
};
this.users = {

View file

@ -59,6 +59,7 @@ import {
BAMBOO_HR_NODE_TYPE,
GOOGLE_SHEETS_NODE_TYPE,
CODE_NODE_TYPE,
ROLE,
} from '@/constants';
import type {
IPersonalizationSurveyAnswersV1,
@ -83,18 +84,13 @@ function isPersonalizationSurveyV2OrLater(
return 'version' in data;
}
export const ROLE = {
Owner: 'global:owner',
Member: 'global:member',
Admin: 'global:admin',
Default: 'default', // default user with no email when setting up instance
} as const;
export const LOGIN_STATUS: { LoggedIn: ILogInStatus; LoggedOut: ILogInStatus } = {
LoggedIn: 'LoggedIn', // Can be owner or member or default user
LoggedOut: 'LoggedOut', // Can only be logged out if UM has been setup
};
export const isUserGlobalOwner = (user: IUser): boolean => user.role === ROLE.Owner;
export function getPersonalizedNodeTypes(
answers:
| IPersonalizationSurveyAnswersV1

View file

@ -8,7 +8,6 @@ import { i18n as locale } from '@/plugins/i18n';
import { useUIStore } from '@/stores/ui.store';
import { N8N_PRICING_PAGE_URL } from '@/constants';
import { useToast } from '@/composables/useToast';
import { ROLE } from '@/utils/userUtils';
import { hasPermission } from '@/rbac/permissions';
const usageStore = useUsageStore();
@ -29,9 +28,7 @@ const activationKey = ref('');
const activationKeyInput = ref<HTMLInputElement | null>(null);
const canUserActivateLicense = computed(() =>
hasPermission(['role'], {
role: [ROLE.Owner],
}),
hasPermission(['rbac'], { rbac: { scope: 'license:manage' } }),
);
const showActivationSuccess = () => {

View file

@ -87,7 +87,7 @@
<script lang="ts">
import { defineComponent } from 'vue';
import { mapStores } from 'pinia';
import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS } from '@/constants';
import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS, ROLE } from '@/constants';
import type { IUser, IUserListAction, InvitableRoleName } from '@/Interface';
import { useToast } from '@/composables/useToast';
@ -97,7 +97,6 @@ import { useUsersStore } from '@/stores/users.store';
import { useUsageStore } from '@/stores/usage.store';
import { useSSOStore } from '@/stores/sso.store';
import { hasPermission } from '@/rbac/permissions';
import { ROLE } from '@/utils/userUtils';
import { useClipboard } from '@/composables/useClipboard';
import type { UpdateGlobalRolePayload } from '@/api/users';
@ -324,3 +323,4 @@ export default defineComponent({
left: calc(50% + 100px);
}
</style>
IRole,

View file

@ -5,6 +5,7 @@ import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store';
import { createComponentRenderer } from '@/__tests__/render';
import { setupServer } from '@/__tests__/server';
import { ROLE } from '@/constants';
let pinia: ReturnType<typeof createPinia>;
let settingsStore: ReturnType<typeof useSettingsStore>;
@ -19,7 +20,7 @@ const currentUser = {
lastName: 'Doe',
email: 'joh.doe@example.com',
createdAt: Date().toString(),
isOwner: true,
role: ROLE.Owner,
isDefaultUser: false,
isPendingUser: false,
isPending: false,