mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-02 07:01:30 -08:00
feat: Add user role select to users list settings page (#7796)
![CleanShot 2023-11-27 at 07 20 58](https://github.com/n8n-io/n8n/assets/5410822/40be0505-32ee-48a7-923e-ba6b4dbce670)
This commit is contained in:
parent
5acb7b94c0
commit
137e23853f
|
@ -35,10 +35,11 @@ export type Scope =
|
|||
| SourceControlScope
|
||||
| ExternalSecretStoreScope;
|
||||
|
||||
export type ScopeLevel<T extends 'global' | 'project' | 'resource'> = Record<T, Scope[]>;
|
||||
export type GlobalScopes = ScopeLevel<'global'>;
|
||||
export type ProjectScopes = ScopeLevel<'project'>;
|
||||
export type ResourceScopes = ScopeLevel<'resource'>;
|
||||
export type ScopeLevel = 'global' | 'project' | 'resource';
|
||||
export type GetScopeLevel<T extends ScopeLevel> = Record<T, Scope[]>;
|
||||
export type GlobalScopes = GetScopeLevel<'global'>;
|
||||
export type ProjectScopes = GetScopeLevel<'project'>;
|
||||
export type ResourceScopes = GetScopeLevel<'resource'>;
|
||||
export type ScopeLevels = GlobalScopes & (ProjectScopes | (ProjectScopes & ResourceScopes));
|
||||
|
||||
export type ScopeMode = 'oneOf' | 'allOf';
|
||||
|
|
|
@ -670,7 +670,7 @@ export type IPersonalizationSurveyVersions =
|
|||
| IPersonalizationSurveyAnswersV2
|
||||
| IPersonalizationSurveyAnswersV3;
|
||||
|
||||
export type IRole = 'default' | 'owner' | 'member';
|
||||
export type IRole = 'default' | 'owner' | 'member' | 'admin';
|
||||
|
||||
export interface IUserResponse {
|
||||
id: string;
|
||||
|
|
|
@ -2,10 +2,12 @@ import type {
|
|||
CurrentUserResponse,
|
||||
IPersonalizationLatestVersion,
|
||||
IRestApiContext,
|
||||
IRole,
|
||||
IUserResponse,
|
||||
} from '@/Interface';
|
||||
import type { IDataObject } from 'n8n-workflow';
|
||||
import { makeRestApiRequest } from '@/utils/apiUtils';
|
||||
import type { ScopeLevel } from '@n8n/permissions';
|
||||
|
||||
export async function loginCurrentUser(
|
||||
context: IRestApiContext,
|
||||
|
@ -143,3 +145,10 @@ export async function submitPersonalizationSurvey(
|
|||
): Promise<void> {
|
||||
await makeRestApiRequest(context, 'POST', '/me/survey', params as unknown as IDataObject);
|
||||
}
|
||||
|
||||
export async function updateRole(
|
||||
context: IRestApiContext,
|
||||
{ id, role }: { id: string; role: { scope: ScopeLevel; name: IRole } },
|
||||
): Promise<IUserResponse> {
|
||||
return makeRestApiRequest(context, 'PATCH', `/users/${id}/role`, { newRole: role });
|
||||
}
|
||||
|
|
|
@ -132,6 +132,10 @@ export default defineComponent({
|
|||
value: ROLE.Member,
|
||||
label: this.$locale.baseText('auth.roles.member'),
|
||||
},
|
||||
{
|
||||
value: ROLE.Admin,
|
||||
label: this.$locale.baseText('auth.roles.admin'),
|
||||
},
|
||||
],
|
||||
capitalize: true,
|
||||
},
|
||||
|
|
|
@ -103,6 +103,7 @@
|
|||
"auth.password": "Password",
|
||||
"auth.role": "Role",
|
||||
"auth.roles.member": "Member",
|
||||
"auth.roles.admin": "Admin",
|
||||
"auth.roles.owner": "Owner",
|
||||
"auth.agreement.label": "Inform me about security vulnerabilities if they arise",
|
||||
"auth.setup.next": "Next",
|
||||
|
|
|
@ -476,9 +476,11 @@ export const routes = [
|
|||
settingsView: SettingsUsersView,
|
||||
},
|
||||
meta: {
|
||||
middleware: ['authenticated', 'role'],
|
||||
middleware: ['authenticated', 'rbac'],
|
||||
middlewareOptions: {
|
||||
role: [ROLE.Owner],
|
||||
rbac: {
|
||||
scope: ['user:create', 'user:update'],
|
||||
},
|
||||
},
|
||||
telemetry: {
|
||||
pageCategory: 'settings',
|
||||
|
|
|
@ -15,6 +15,7 @@ import {
|
|||
updateOtherUserSettings,
|
||||
validatePasswordToken,
|
||||
validateSignupToken,
|
||||
updateRole,
|
||||
} from '@/api/users';
|
||||
import { PERSONALIZATION_MODAL_KEY, STORES } from '@/constants';
|
||||
import type {
|
||||
|
@ -39,16 +40,13 @@ import { useCloudPlanStore } from './cloudPlan.store';
|
|||
import { disableMfa, enableMfa, getMfaQR, verifyMfaToken } from '@/api/mfa';
|
||||
import { confirmEmail, getCloudUserInfo } from '@/api/cloudPlans';
|
||||
import { useRBACStore } from '@/stores/rbac.store';
|
||||
import type { Scope } from '@n8n/permissions';
|
||||
import type { Scope, ScopeLevel } from '@n8n/permissions';
|
||||
import { inviteUsers, acceptInvitation } from '@/api/invitation';
|
||||
|
||||
const isDefaultUser = (user: IUserResponse | null) =>
|
||||
Boolean(user && user.isPending && user.globalRole && user.globalRole.name === ROLE.Owner);
|
||||
|
||||
const isPendingUser = (user: IUserResponse | null) => Boolean(user && user.isPending);
|
||||
|
||||
const isInstanceOwner = (user: IUserResponse | null) =>
|
||||
Boolean(user?.globalRole?.name === ROLE.Owner);
|
||||
user?.isPending && user?.globalRole?.name === ROLE.Owner;
|
||||
const isPendingUser = (user: IUserResponse | null) => !!user?.isPending;
|
||||
const isInstanceOwner = (user: IUserResponse | null) => user?.globalRole?.name === ROLE.Owner;
|
||||
|
||||
export const useUsersStore = defineStore(STORES.USERS, {
|
||||
state: (): IUsersState => ({
|
||||
|
@ -375,5 +373,11 @@ export const useUsersStore = defineStore(STORES.USERS, {
|
|||
async confirmEmail() {
|
||||
await confirmEmail(useRootStore().getRestApiContext);
|
||||
},
|
||||
|
||||
async updateRole({ id, role }: { id: string; role: { scope: ScopeLevel; name: IRole } }) {
|
||||
const rootStore = useRootStore();
|
||||
await updateRole(rootStore.getRestApiContext, { id, role });
|
||||
await this.fetchUsers();
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -84,9 +84,11 @@ function isPersonalizationSurveyV2OrLater(
|
|||
return 'version' in data;
|
||||
}
|
||||
|
||||
export const ROLE: { Owner: IRole; Member: IRole; Default: IRole } = {
|
||||
export type Roles = { [R in IRole as Capitalize<R>]: R };
|
||||
export const ROLE: Roles = {
|
||||
Owner: 'owner',
|
||||
Member: 'member',
|
||||
Admin: 'admin',
|
||||
Default: 'default', // default user with no email when setting up instance
|
||||
};
|
||||
|
||||
|
|
|
@ -52,7 +52,23 @@
|
|||
@copyPasswordResetLink="onCopyPasswordResetLink"
|
||||
@allowSSOManualLogin="onAllowSSOManualLogin"
|
||||
@disallowSSOManualLogin="onDisallowSSOManualLogin"
|
||||
/>
|
||||
>
|
||||
<template #actions="{ user }">
|
||||
<n8n-select
|
||||
:modelValue="user.globalRole.name"
|
||||
@update:modelValue="($event: IRole) => onRoleChange(user, $event)"
|
||||
:disabled="!canUpdateRole"
|
||||
data-test-id="user-role-select"
|
||||
>
|
||||
<n8n-option
|
||||
v-for="role in userRoles"
|
||||
:key="role.value"
|
||||
:value="role.value"
|
||||
:label="role.label"
|
||||
/>
|
||||
</n8n-select>
|
||||
</template>
|
||||
</n8n-users-list>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -62,7 +78,7 @@ import { defineComponent } from 'vue';
|
|||
import { mapStores } from 'pinia';
|
||||
import { EnterpriseEditionFeature, INVITE_USER_MODAL_KEY, VIEWS } from '@/constants';
|
||||
|
||||
import type { IUserListAction } from '@/Interface';
|
||||
import type { IRole, IUser, IUserListAction } from '@/Interface';
|
||||
import { useToast } from '@/composables';
|
||||
import { copyPaste } from '@/mixins/copyPaste';
|
||||
import { useUIStore } from '@/stores/ui.store';
|
||||
|
@ -133,6 +149,21 @@ export default defineComponent({
|
|||
},
|
||||
];
|
||||
},
|
||||
userRoles(): Array<{ value: IRole; label: string }> {
|
||||
return [
|
||||
{
|
||||
value: ROLE.Member,
|
||||
label: this.$locale.baseText('auth.roles.member'),
|
||||
},
|
||||
{
|
||||
value: ROLE.Admin,
|
||||
label: this.$locale.baseText('auth.roles.admin'),
|
||||
},
|
||||
];
|
||||
},
|
||||
canUpdateRole(): boolean {
|
||||
return hasPermission(['rbac'], { rbac: { scope: 'user:update' } });
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
redirectToSetup() {
|
||||
|
@ -220,6 +251,9 @@ export default defineComponent({
|
|||
goToUpgrade() {
|
||||
void this.uiStore.goToUpgrade('settings-users', 'upgrade-users');
|
||||
},
|
||||
async onRoleChange(user: IUser, name: IRole) {
|
||||
await this.usersStore.updateRole({ id: user.id, role: { scope: 'global', name } });
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
Loading…
Reference in a new issue