feat(API): Add user management endpoints to the Projects Public API (#12329)

Co-authored-by: कारतोफ्फेलस्क्रिप्ट™ <aditya@netroy.in>
Co-authored-by: Danny Martini <danny@n8n.io>
Co-authored-by: Andreas Fitzek <andreas.fitzek@n8n.io>
Co-authored-by: Guillaume Jacquart <jacquart.guillaume@gmail.com>
This commit is contained in:
Marc Littlemore
2025-05-30 12:04:38 +01:00
committed by GitHub
parent c229e915ea
commit 4459c7e7b1
25 changed files with 1391 additions and 71 deletions

View File

@@ -28,6 +28,8 @@ export { ChangePasswordRequestDto } from './password-reset/change-password-reque
export { CreateProjectDto } from './project/create-project.dto';
export { UpdateProjectDto } from './project/update-project.dto';
export { DeleteProjectDto } from './project/delete-project.dto';
export { AddUsersToProjectDto } from './project/add-users-to-project.dto';
export { ChangeUserRoleInProject } from './project/change-user-role-in-project.dto';
export { SamlAcsDto } from './saml/saml-acs.dto';
export { SamlPreferences } from './saml/saml-preferences.dto';

View File

@@ -0,0 +1,143 @@
import { AddUsersToProjectDto } from '../add-users-to-project.dto';
describe('AddUsersToProjectDto', () => {
describe('Valid requests', () => {
test.each([
{
name: 'with single user',
request: {
relations: [
{
userId: 'user-123',
role: 'project:admin',
},
],
},
},
{
name: 'with multiple relations',
request: {
relations: [
{
userId: 'user-123',
role: 'project:admin',
},
{
userId: 'user-456',
role: 'project:editor',
},
{
userId: 'user-789',
role: 'project:viewer',
},
],
},
},
{
name: 'with all possible roles unless the `project:personalOwner`',
request: {
relations: [
{ userId: 'user-1', role: 'project:admin' },
{ userId: 'user-2', role: 'project:editor' },
{ userId: 'user-3', role: 'project:viewer' },
],
},
},
])('should validate $name', ({ request }) => {
const result = AddUsersToProjectDto.safeParse(request);
expect(result.success).toBe(true);
});
});
describe('Invalid requests', () => {
test.each([
{
name: 'missing relations array',
request: {},
expectedErrorPath: ['relations'],
},
{
name: 'empty relations array',
request: {
relations: [],
},
expectedErrorPath: ['relations'],
},
{
name: 'invalid userId type',
request: {
relations: [
{
userId: 123,
role: 'project:admin',
},
],
},
expectedErrorPath: ['relations', 0, 'userId'],
},
{
name: 'empty userId',
request: {
relations: [
{
userId: '',
role: 'project:admin',
},
],
},
expectedErrorPath: ['relations', 0, 'userId'],
},
{
name: 'invalid role',
request: {
relations: [
{
userId: 'user-123',
role: 'invalid-role',
},
],
},
expectedErrorPath: ['relations', 0, 'role'],
},
{
name: 'missing role',
request: {
relations: [
{
userId: 'user-123',
},
],
},
expectedErrorPath: ['relations', 0, 'role'],
},
{
name: 'invalid relations array type',
request: {
relations: 'not-an-array',
},
expectedErrorPath: ['relations'],
},
{
name: 'invalid user object in array',
request: {
relations: ['not-an-object'],
},
expectedErrorPath: ['relations', 0],
},
{
name: 'invalid with `project:personalOwner` role',
request: {
relations: [{ userId: 'user-1', role: 'project:personalOwner' }],
},
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = AddUsersToProjectDto.safeParse(request);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
}
});
});
});

View File

@@ -0,0 +1,54 @@
import { ChangeUserRoleInProject } from '../change-user-role-in-project.dto';
describe('ChangeUserRoleInProject', () => {
describe('Allow valid roles', () => {
test.each(['project:admin', 'project:editor', 'project:viewer'])('should allow %s', (role) => {
const result = ChangeUserRoleInProject.safeParse({ role });
expect(result.success).toBe(true);
});
});
describe('Reject invalid roles', () => {
test.each([
{
name: 'missing role',
request: {},
expectedErrorPath: ['role'],
},
{
name: 'empty role',
request: {
role: '',
},
expectedErrorPath: ['role'],
},
{
name: 'invalid role type',
request: {
role: 123,
},
expectedErrorPath: ['role'],
},
{
name: 'invalid role value',
request: {
role: 'invalid-role',
},
expectedErrorPath: ['role'],
},
{
name: 'personal owner role',
request: { role: 'project:personalOwner' },
expectedErrorPath: ['role'],
},
])('should reject $name', ({ request, expectedErrorPath }) => {
const result = ChangeUserRoleInProject.safeParse(request);
expect(result.success).toBe(false);
if (expectedErrorPath) {
expect(result.error?.issues[0].path).toEqual(expectedErrorPath);
}
});
});
});

View File

@@ -0,0 +1,8 @@
import { z } from 'zod';
import { Z } from 'zod-class';
import { projectRelationSchema } from '../../schemas/project.schema';
export class AddUsersToProjectDto extends Z.class({
relations: z.array(projectRelationSchema).min(1),
}) {}

View File

@@ -0,0 +1,6 @@
import { projectRoleSchema } from '@n8n/permissions';
import { Z } from 'zod-class';
export class ChangeUserRoleInProject extends Z.class({
role: projectRoleSchema.exclude(['project:personalOwner']),
}) {}

View File

@@ -13,7 +13,7 @@ export const projectIconSchema = z.object({
export type ProjectIcon = z.infer<typeof projectIconSchema>;
export const projectRelationSchema = z.object({
userId: z.string(),
role: projectRoleSchema,
userId: z.string().min(1),
role: projectRoleSchema.exclude(['project:personalOwner']),
});
export type ProjectRelation = z.infer<typeof projectRelationSchema>;