remove legacy code

This commit is contained in:
Stephen
2026-03-16 15:41:46 +00:00
parent 8dabe8a793
commit 5bab29e046
12 changed files with 64 additions and 467 deletions

View File

@@ -1,17 +1,8 @@
import { ResolveSignupTokenQueryDto } from '../resolve-signup-token-query.dto';
describe('ResolveSignupTokenQueryDto', () => {
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
describe('Valid requests', () => {
test.each([
{
name: 'legacy format with both inviterId and inviteeId',
request: {
inviterId: validUuid,
inviteeId: validUuid,
},
},
{
name: 'JWT token format',
request: {
@@ -19,18 +10,6 @@ describe('ResolveSignupTokenQueryDto', () => {
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpbnZpdGVySWQiOiIxMjNlNDU2Ny1lODliLTEyZDMtYTQ1Ni00MjY2MTQxNzQwMDAiLCJpbnZpdGVlSWQiOiIxMjNlNDU2Ny1lODliLTEyZDMtYTQ1Ni00MjY2MTQxNzQwMDAifQ.test',
},
},
{
name: 'missing inviterId (could be token-based)',
request: {
inviteeId: validUuid,
},
},
{
name: 'missing inviteeId (could be token-based)',
request: {
inviterId: validUuid,
},
},
])('should validate $name', ({ request }) => {
const result = ResolveSignupTokenQueryDto.safeParse(request);
expect(result.success).toBe(true);
@@ -40,44 +19,16 @@ describe('ResolveSignupTokenQueryDto', () => {
describe('Invalid requests', () => {
test.each([
{
name: 'invalid inviterId UUID',
request: {
inviterId: 'not-a-valid-uuid',
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
name: 'missing token',
request: {},
expectedErrorPath: ['token'],
},
{
name: 'invalid inviteeId UUID',
name: 'empty token',
request: {
inviterId: validUuid,
inviteeId: 'not-a-valid-uuid',
token: '',
},
expectedErrorPath: ['inviteeId'],
},
{
name: 'UUID with invalid characters',
request: {
inviterId: '123e4567-e89b-12d3-a456-42661417400G',
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
},
{
name: 'UUID too long',
request: {
inviterId: '123e4567-e89b-12d3-a456-426614174001234',
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
},
{
name: 'UUID too short',
request: {
inviterId: '123e4567-e89b-12d3-a456',
inviteeId: validUuid,
},
expectedErrorPath: ['inviterId'],
expectedErrorPath: ['token'],
},
])('should fail validation for $name', ({ request, expectedErrorPath }) => {
const result = ResolveSignupTokenQueryDto.safeParse(request);

View File

@@ -2,12 +2,9 @@ import { z } from 'zod';
import { Z } from '../../zod-class';
// Support both legacy format (inviterId + inviteeId) and new JWT format (token)
// All fields are optional at the schema level, but validation ensures either token OR (inviterId AND inviteeId) are provided
// Only support JWT token-based invites (tamper-proof)
const resolveSignupTokenShape = {
inviterId: z.string().uuid().optional(),
inviteeId: z.string().uuid().optional(),
token: z.string().optional(),
token: z.string().min(1, 'Token is required'),
};
export class ResolveSignupTokenQueryDto extends Z.class(resolveSignupTokenShape) {}

View File

@@ -3,12 +3,9 @@ import { z } from 'zod';
import { passwordSchema } from '../../schemas/password.schema';
import { Z } from '../../zod-class';
// Support both legacy format (inviterId) and new JWT format (token)
// All fields are optional at the schema level, but validation ensures either token OR inviterId is provided
// Only support JWT token-based invites (tamper-proof)
export class AcceptInvitationRequestDto extends Z.class({
inviterId: z.string().uuid().optional(),
inviteeId: z.string().uuid().optional(),
token: z.string().optional(),
token: z.string().min(1, 'Token is required'),
firstName: z.string().min(1, 'First name is required'),
lastName: z.string().min(1, 'Last name is required'),
password: passwordSchema,

View File

@@ -203,7 +203,7 @@ describe('AuthController', () => {
it('throws a BadRequestError if SSO is enabled', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(true);
const id = uuidv4();
const token = 'valid-jwt-token';
const authController = new AuthController(
logger,
@@ -218,8 +218,7 @@ describe('AuthController', () => {
);
const payload = new ResolveSignupTokenQueryDto({
inviterId: id,
inviteeId: id,
token,
});
const req = mock<AuthlessRequest>({
@@ -237,6 +236,7 @@ describe('AuthController', () => {
it('throws a ForbiddenError if the users quota is reached', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
const id = uuidv4();
const token = 'valid-jwt-token';
const authController = new AuthController(
logger,
@@ -251,8 +251,7 @@ describe('AuthController', () => {
);
const payload = new ResolveSignupTokenQueryDto({
inviterId: id,
inviteeId: id,
token,
});
const req = mock<AuthlessRequest>({
@@ -274,6 +273,7 @@ describe('AuthController', () => {
it('throws a BadRequestError if the users are not found', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
const id = uuidv4();
const token = 'valid-jwt-token';
const authController = new AuthController(
logger,
@@ -288,8 +288,7 @@ describe('AuthController', () => {
);
const payload = new ResolveSignupTokenQueryDto({
inviterId: id,
inviteeId: id,
token,
});
const req = mock<AuthlessRequest>({
@@ -312,6 +311,7 @@ describe('AuthController', () => {
it('throws a BadRequestError if the invitee already has a password', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
const id = uuidv4();
const token = 'valid-jwt-token';
const authController = new AuthController(
logger,
@@ -326,8 +326,7 @@ describe('AuthController', () => {
);
const payload = new ResolveSignupTokenQueryDto({
inviterId: id,
inviteeId: id,
token,
});
const req = mock<AuthlessRequest>({
@@ -359,6 +358,7 @@ describe('AuthController', () => {
it('throws a BadRequestError if the inviter does not exist or is not set up', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
const id = uuidv4();
const token = 'valid-jwt-token';
const authController = new AuthController(
logger,
@@ -373,8 +373,7 @@ describe('AuthController', () => {
);
const payload = new ResolveSignupTokenQueryDto({
inviterId: id,
inviteeId: id,
token,
});
const req = mock<AuthlessRequest>({
@@ -408,6 +407,7 @@ describe('AuthController', () => {
it('returns the inviter if the invitation is valid', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
const id = uuidv4();
const token = 'valid-jwt-token';
const authController = new AuthController(
logger,
@@ -422,8 +422,7 @@ describe('AuthController', () => {
);
const payload = new ResolveSignupTokenQueryDto({
inviterId: id,
inviteeId: id,
token,
});
const req = mock<AuthlessRequest>({
@@ -517,7 +516,7 @@ describe('AuthController', () => {
},
});
expect(userService.getInvitationIdsFromPayload).toHaveBeenCalledWith(payload);
expect(userService.getInvitationIdsFromPayload).toHaveBeenCalledWith(token);
});
it('throws BadRequestError if JWT token is invalid', async () => {
@@ -588,7 +587,7 @@ describe('AuthController', () => {
);
});
it('throws BadRequestError if neither token nor inviterId/inviteeId are provided', async () => {
it('throws BadRequestError if token is missing', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
const authController = new AuthController(
@@ -603,7 +602,7 @@ describe('AuthController', () => {
postHog,
);
// Create empty payload to test validation when neither token nor IDs are provided
// Create empty payload to test validation when token is missing
// Use type assertion to bypass zod-class validation for all-optional schemas
// Validation is handled in the service layer
const payload = {} as ResolveSignupTokenQueryDto;
@@ -613,12 +612,8 @@ describe('AuthController', () => {
});
const res = mock<Response>();
jest
.spyOn(userService, 'getInvitationIdsFromPayload')
.mockRejectedValue(new BadRequestError('Invalid invite URL'));
await expect(authController.resolveSignupToken(req, res, payload)).rejects.toThrow(
new BadRequestError('Invalid invite URL'),
new BadRequestError('Token is required'),
);
});
});

View File

@@ -225,229 +225,6 @@ describe('InvitationController', () => {
});
});
describe('acceptInvitation', () => {
it('throws a BadRequestError if SSO is enabled', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(true);
jest.spyOn(ownershipService, 'hasInstanceOwner').mockReturnValue(Promise.resolve(true));
const inviterId = uuidv4();
const inviteeId = uuidv4();
const invitationController = defaultInvitationController();
const payload = new AcceptInvitationRequestDto({
inviterId,
firstName: 'John',
lastName: 'Doe',
password: 'Password123!',
});
const req = mock<AuthlessRequest<{ id: string }>>({
body: payload,
params: { id: inviteeId },
});
const res = mock<Response>();
await expect(
invitationController.acceptInvitation(req, res, payload, inviteeId),
).rejects.toThrow(
new BadRequestError(
'Invite links are not supported on this system, please use single sign on instead.',
),
);
});
it('throws a BadRequestError if the inviter ID and invitee ID are not found in the database', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
jest.spyOn(ownershipService, 'hasInstanceOwner').mockReturnValue(Promise.resolve(true));
const inviterId = uuidv4();
const inviteeId = uuidv4();
const invitationController = defaultInvitationController();
const payload = new AcceptInvitationRequestDto({
inviterId,
firstName: 'John',
lastName: 'Doe',
password: 'Password123!',
});
const req = mock<AuthlessRequest<{ id: string }>>({
body: payload,
params: { id: inviteeId },
});
const res = mock<Response>();
jest.spyOn(userRepository, 'find').mockResolvedValue([]);
await expect(
invitationController.acceptInvitation(req, res, payload, inviteeId),
).rejects.toThrow(new BadRequestError('Invalid payload or URL'));
expect(userRepository.find).toHaveBeenCalledWith({
where: [{ id: inviterId }, { id: inviteeId }],
relations: ['role'],
});
});
it('throws a BadRequestError if the invitee already has a password', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
jest.spyOn(ownershipService, 'hasInstanceOwner').mockReturnValue(Promise.resolve(true));
const inviteeId = uuidv4();
const invitee = mock<User>({
id: inviteeId,
email: 'valid@email.com',
password: 'Password123!',
role: GLOBAL_MEMBER_ROLE,
});
const inviterId = uuidv4();
const inviter = mock<User>({
id: inviterId,
email: 'valid@email.com',
role: GLOBAL_OWNER_ROLE,
});
jest.spyOn(userRepository, 'find').mockResolvedValue([inviter, invitee]);
const invitationController = defaultInvitationController();
const payload = new AcceptInvitationRequestDto({
inviterId,
firstName: 'John',
lastName: 'Doe',
password: 'Password123!',
});
const req = mock<AuthlessRequest<{ id: string }>>({
body: payload,
params: { id: inviteeId },
});
const res = mock<Response>();
await expect(
invitationController.acceptInvitation(req, res, payload, inviteeId),
).rejects.toThrow(new BadRequestError('This invite has been accepted already'));
});
it('accepts the invitation successfully', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
jest.spyOn(ownershipService, 'hasInstanceOwner').mockReturnValue(Promise.resolve(true));
const inviterId = uuidv4();
const inviteeId = uuidv4();
const inviter = mock<User>({
id: inviterId,
email: 'valid@email.com',
role: GLOBAL_OWNER_ROLE,
});
const invitee = mock<User>({
id: inviteeId,
email: 'valid@email.com',
password: null,
role: GLOBAL_MEMBER_ROLE,
});
jest.spyOn(userRepository, 'find').mockResolvedValue([inviter, invitee]);
jest.spyOn(passwordUtility, 'hash').mockResolvedValue('Password123!');
jest.spyOn(userRepository, 'save').mockResolvedValue(invitee);
jest.spyOn(authService, 'issueCookie').mockResolvedValue(invitee as never);
jest.spyOn(eventService, 'emit').mockResolvedValue(invitee as never);
jest.spyOn(userService, 'toPublic').mockResolvedValue(invitee as unknown as PublicUser);
jest.spyOn(externalHooks, 'run').mockResolvedValue(invitee as never);
const invitationController = defaultInvitationController();
const payload = new AcceptInvitationRequestDto({
inviterId,
firstName: 'John',
lastName: 'Doe',
password: 'Password123!',
});
const req = mock<AuthlessRequest<{ id: string }>>({
body: payload,
params: { id: inviteeId },
});
const res = mock<Response>();
expect(await invitationController.acceptInvitation(req, res, payload, inviteeId)).toEqual(
invitee as unknown as PublicUser,
);
});
it('accepts the invitation successfully with legacy inviterId and inviteeId from URL', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
jest.spyOn(ownershipService, 'hasInstanceOwner').mockReturnValue(Promise.resolve(true));
const inviterId = uuidv4();
const inviteeId = uuidv4();
const inviter = mock<User>({
id: inviterId,
email: 'valid@email.com',
role: GLOBAL_OWNER_ROLE,
});
const invitee = mock<User>({
id: inviteeId,
email: 'valid@email.com',
password: null,
role: GLOBAL_MEMBER_ROLE,
});
jest.spyOn(userRepository, 'find').mockResolvedValue([inviter, invitee]);
jest.spyOn(passwordUtility, 'hash').mockResolvedValue('Password123!');
jest.spyOn(userRepository, 'save').mockResolvedValue(invitee);
jest.spyOn(authService, 'issueCookie').mockResolvedValue(invitee as never);
jest.spyOn(eventService, 'emit').mockResolvedValue(invitee as never);
jest.spyOn(userService, 'toPublic').mockResolvedValue(invitee as unknown as PublicUser);
jest.spyOn(externalHooks, 'run').mockResolvedValue(invitee as never);
const invitationController = defaultInvitationController();
const payload = new AcceptInvitationRequestDto({
inviterId,
firstName: 'John',
lastName: 'Doe',
password: 'Password123!',
});
const req = mock<AuthlessRequest<{ id: string }>>({
body: payload,
params: { id: inviteeId },
});
const res = mock<Response>();
expect(await invitationController.acceptInvitation(req, res, payload, inviteeId)).toEqual(
invitee as unknown as PublicUser,
);
});
it('throws a BadRequestError if inviterId or inviteeId is missing in legacy format', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(false);
jest.spyOn(ownershipService, 'hasInstanceOwner').mockReturnValue(Promise.resolve(true));
const invitationController = defaultInvitationController();
const payload = {
firstName: 'John',
lastName: 'Doe',
password: 'Password123!',
} as AcceptInvitationRequestDto;
const req = mock<AuthlessRequest<{ id: string }>>({
body: payload,
params: { id: uuidv4() },
});
const res = mock<Response>();
await expect(
invitationController.acceptInvitation(req, res, payload, req.params.id),
).rejects.toThrow(new BadRequestError('InviterId and inviteeId are required'));
});
});
describe('acceptInvitationWithToken', () => {
it('throws a BadRequestError if SSO is enabled', async () => {
jest.spyOn(ssoHelpers, 'isSsoCurrentAuthenticationMethod').mockReturnValue(true);
@@ -545,9 +322,7 @@ describe('InvitationController', () => {
invitee as unknown as PublicUser,
);
expect(userService.getInvitationIdsFromPayload).toHaveBeenCalledWith({
token,
});
expect(userService.getInvitationIdsFromPayload).toHaveBeenCalledWith(token);
expect(userRepository.find).toHaveBeenCalledWith({
where: [{ id: inviterId }, { id: inviteeId }],
relations: ['role'],

View File

@@ -211,7 +211,14 @@ export class AuthController {
);
}
const { inviterId, inviteeId } = await this.userService.getInvitationIdsFromPayload(payload);
if (!payload.token) {
this.logger.debug('Request to resolve signup token failed because token is missing');
throw new BadRequestError('Token is required');
}
const { inviterId, inviteeId } = await this.userService.getInvitationIdsFromPayload(
payload.token,
);
const isWithinUsersLimit = this.license.isWithinUsersLimit();

View File

@@ -2,14 +2,7 @@ import { AcceptInvitationRequestDto, InviteUsersRequestDto } from '@n8n/api-type
import { Logger } from '@n8n/backend-common';
import type { User } from '@n8n/db';
import { UserRepository, AuthenticatedRequest } from '@n8n/db';
import {
Post,
GlobalScope,
RestController,
Body,
Param,
createBodyKeyedRateLimiter,
} from '@n8n/decorators';
import { Post, GlobalScope, RestController, Body } from '@n8n/decorators';
import { Response } from 'express';
import { AuthService } from '@/auth/auth.service';
@@ -190,60 +183,9 @@ export class InvitationController {
const { firstName, lastName, password } = payload;
// Extract inviterId and inviteeId from JWT token
const { inviterId, inviteeId } = await this.userService.getInvitationIdsFromPayload({
token: payload.token,
});
return await this.processInvitationAcceptance(
inviterId,
inviteeId,
firstName,
lastName,
password,
req,
res,
const { inviterId, inviteeId } = await this.userService.getInvitationIdsFromPayload(
payload.token,
);
}
/**
* Fill out user shell with first name, last name, and password (legacy format with inviterId/inviteeId).
*/
@Post('/:id/accept', {
skipAuth: true,
// Two layered rate limit to ensure multiple users can accept an invitation from
// the same IP address but aggressive per inviteeId limit.
ipRateLimit: { limit: 100, windowMs: 5 * Time.minutes.toMilliseconds },
keyedRateLimit: createBodyKeyedRateLimiter<AcceptInvitationRequestDto>({
limit: 10,
windowMs: 1 * Time.minutes.toMilliseconds,
field: 'inviterId',
}),
})
async acceptInvitation(
req: AuthlessRequest,
res: Response,
@Body payload: AcceptInvitationRequestDto,
@Param('id') inviteeId: string,
) {
if (isSsoCurrentAuthenticationMethod()) {
this.logger.debug(
'Invite links are not supported on this system, please use single sign on instead.',
);
throw new BadRequestError(
'Invite links are not supported on this system, please use single sign on instead.',
);
}
if (!payload.inviterId || !inviteeId) {
this.logger.debug(
'Request to accept invitation failed because inviterId or inviteeId is missing',
);
throw new BadRequestError('InviterId and inviteeId are required');
}
const { firstName, lastName, password } = payload;
const inviterId = payload.inviterId;
return await this.processInvitationAcceptance(
inviterId,

View File

@@ -414,11 +414,10 @@ export class UserService {
}
/**
* Extract inviterId and inviteeId from either JWT token or legacy query parameters
* Validates the format based on the feature flag for the inviter
* @param payload - ResolveSignupTokenQueryDto containing either token or inviterId/inviteeId
* Extract inviterId and inviteeId from JWT token
* @param token - JWT token containing inviterId and inviteeId
* @returns Object with inviterId and inviteeId
* @throws BadRequestError if format doesn't match feature flag, JWT is invalid, or required parameters are missing
* @throws BadRequestError if JWT is invalid or required parameters are missing
*/
private async processTokenBasedInvite(
token: string,
@@ -440,23 +439,15 @@ export class UserService {
}
}
private async processInviteeIdInviterIdBasedInvite(
inviterId: string,
inviteeId: string,
/**
* Extract inviterId and inviteeId from JWT token
* @param token - JWT token containing inviterId and inviteeId
* @returns Object with inviterId and inviteeId
* @throws BadRequestError if JWT is invalid or required parameters are missing
*/
async getInvitationIdsFromPayload(
token: string,
): Promise<{ inviterId: string; inviteeId: string }> {
return { inviterId, inviteeId };
}
async getInvitationIdsFromPayload(payload: {
token?: string;
inviterId?: string;
inviteeId?: string;
}): Promise<{ inviterId: string; inviteeId: string }> {
if (payload.token && (payload.inviteeId || payload.inviterId)) {
this.logger.error('Invalid invite url containing both token and inviterId / inviteeId');
throw new BadRequestError('Invalid invite URL');
}
const instanceOwner = await this.userRepository.findOne({
where: { role: { slug: GLOBAL_OWNER_ROLE.slug } },
});
@@ -465,15 +456,7 @@ export class UserService {
throw new BadRequestError('Instance owner not found');
}
// Always prefer token-based invites (tamper-proof), but support legacy format for backward compatibility
if (payload.token) {
return await this.processTokenBasedInvite(payload.token);
}
if (payload.inviterId && payload.inviteeId) {
return await this.processInviteeIdInviterIdBasedInvite(payload.inviterId, payload.inviteeId);
}
throw new BadRequestError('Invalid invite URL');
// Only support token-based invites (tamper-proof)
return await this.processTokenBasedInvite(token);
}
}

View File

@@ -116,30 +116,11 @@ export async function setupOwner(
export async function validateSignupToken(
context: IRestApiContext,
params: { token?: string } | { inviterId?: string; inviteeId?: string },
params: { token: string },
): Promise<{ inviter: { firstName: string; lastName: string } }> {
return await makeRestApiRequest(context, 'GET', '/resolve-signup-token', params);
}
export async function signup(
context: IRestApiContext,
params: {
inviterId: string;
inviteeId: string;
firstName: string;
lastName: string;
password: string;
},
): Promise<CurrentUserResponse> {
const { inviteeId, ...props } = params;
return await makeRestApiRequest(
context,
'POST',
`/users/${params.inviteeId}`,
props as unknown as IDataObject,
);
}
export async function sendForgotPasswordEmail(
context: IRestApiContext,
params: { email: string },

View File

@@ -65,8 +65,6 @@ const FORM_CONFIG: IFormBoxConfig = {
const loading = ref(false);
const inviter = ref<null | { firstName: string; lastName: string }>(null);
const inviterId = ref<string | undefined>(undefined);
const inviteeId = ref<string | undefined>(undefined);
const token = ref<string | undefined>(undefined);
const inviteMessage = computed(() => {
@@ -80,22 +78,16 @@ const inviteMessage = computed(() => {
});
onMounted(async () => {
const inviterIdParam = getQueryParameter('inviterId');
const inviteeIdParam = getQueryParameter('inviteeId');
const tokenParam = getQueryParameter('token');
try {
if (!tokenParam && !inviterIdParam && !inviteeIdParam) {
if (!tokenParam) {
throw new Error(i18n.baseText('auth.signup.missingTokenError'));
}
inviterId.value = inviterIdParam ?? undefined;
inviteeId.value = inviteeIdParam ?? undefined;
token.value = tokenParam ?? undefined;
token.value = tokenParam;
const invite = await usersStore.validateSignupToken({
inviteeId: inviteeId.value,
inviterId: inviterId.value,
token: token.value,
});
inviter.value = invite.inviter as { firstName: string; lastName: string };
@@ -106,8 +98,7 @@ onMounted(async () => {
});
async function onSubmit(values: { [key: string]: string | boolean }) {
if (!token.value && (!inviterId.value || !inviteeId.value)) {
// Legacy invitation: require both inviterId and inviteeId
if (!token.value) {
toast.showError(
new Error(i18n.baseText('auth.signup.tokenValidationError')),
i18n.baseText('auth.signup.setupYourAccountError'),
@@ -119,13 +110,9 @@ async function onSubmit(values: { [key: string]: string | boolean }) {
loading.value = true;
await usersStore.acceptInvitation({
...values,
inviterId: inviterId.value,
inviteeId: inviteeId.value,
token: token.value,
} as {
inviteeId?: string;
inviterId?: string;
token?: string;
token: string;
firstName: string;
lastName: string;
password: string;
@@ -144,7 +131,7 @@ async function onSubmit(values: { [key: string]: string | boolean }) {
loading.value = false;
}
function getQueryParameter(key: 'inviterId' | 'inviteeId' | 'token'): string | null {
function getQueryParameter(key: 'token'): string | null {
return !route.query[key] || typeof route.query[key] !== 'string' ? null : route.query[key];
}
</script>

View File

@@ -5,9 +5,7 @@ import type { IDataObject } from 'n8n-workflow';
import { makeRestApiRequest } from '@n8n/rest-api-client';
type AcceptInvitationParams = {
token?: string;
inviterId?: string;
inviteeId?: string;
token: string;
firstName: string;
lastName: string;
password: string;
@@ -21,26 +19,14 @@ export async function inviteUsers(
}
export async function acceptInvitation(context: IRestApiContext, params: AcceptInvitationParams) {
// Use new /accept endpoint for token-based invitations
if (params.token) {
return await makeRestApiRequest<CurrentUserResponse>(
context,
'POST',
'/invitations/accept',
params as unknown as IDataObject,
);
if (!params.token) {
throw new Error('Token is required');
}
// Use legacy /:id/accept endpoint for inviterId/inviteeId-based invitations
if (!params.inviteeId) {
throw new Error('inviteeId is required when not using token');
}
const { inviteeId, ...props } = params;
return await makeRestApiRequest<CurrentUserResponse>(
context,
'POST',
`/invitations/${inviteeId}/accept`,
props as unknown as IDataObject,
'/invitations/accept',
params as unknown as IDataObject,
);
}

View File

@@ -252,16 +252,12 @@ export const useUsersStore = defineStore(STORES.USERS, () => {
}
};
const validateSignupToken = async (
params: { token?: string } | { inviteeId?: string; inviterId?: string },
) => {
const validateSignupToken = async (params: { token: string }) => {
return await usersApi.validateSignupToken(rootStore.restApiContext, params);
};
const acceptInvitation = async (params: {
token?: string;
inviteeId?: string;
inviterId?: string;
token: string;
firstName: string;
lastName: string;
password: string;