export function inviteUser()

in controlplane/src/core/bufservices/user/inviteUser.ts [12:198]


export function inviteUser(
  opts: RouterOptions,
  req: InviteUserRequest,
  ctx: HandlerContext,
): Promise<PlainMessage<InviteUserResponse>> {
  let logger = getLogger(ctx, opts.logger);

  return handleError<PlainMessage<InviteUserResponse>>(ctx, logger, async () => {
    const authContext = await opts.authenticator.authenticate(ctx.requestHeader);
    logger = enrichLogger(ctx, logger, authContext);

    const userRepo = new UserRepository(logger, opts.db);
    const orgRepo = new OrganizationRepository(logger, opts.db, opts.billingDefaultPlanId);
    const orgInvitationRepo = new OrganizationInvitationRepository(logger, opts.db, opts.billingDefaultPlanId);
    const auditLogRepo = new AuditLogRepository(opts.db);

    if (!authContext.hasWriteAccess) {
      return {
        response: {
          code: EnumStatusCode.ERR,
          details: `The user doesnt have the permissions to perform this operation`,
        },
      };
    }

    const organization = await orgRepo.byId(authContext.organizationId);
    if (!organization) {
      return {
        response: {
          code: EnumStatusCode.ERR_NOT_FOUND,
          details: `Organization not found`,
        },
      };
    }

    const memberCount = await orgRepo.memberCount(authContext.organizationId);

    const usersFeature = await orgRepo.getFeature({
      organizationId: authContext.organizationId,
      featureId: 'users',
    });

    const limit = usersFeature?.limit === -1 ? undefined : usersFeature?.limit;

    if (limit && memberCount >= limit) {
      return {
        response: {
          code: EnumStatusCode.ERR_LIMIT_REACHED,
          details: `The user limit for this organization has been reached`,
        },
      };
    }

    await opts.keycloakClient.authenticateClient();

    const keycloakUser = await opts.keycloakClient.client.users.find({
      max: 1,
      email: req.email,
      realm: opts.keycloakRealm,
      exact: true,
    });

    let keycloakUserID;

    if (keycloakUser.length === 0) {
      keycloakUserID = await opts.keycloakClient.addKeycloakUser({
        email: req.email,
        isPasswordTemp: true,
        realm: opts.keycloakRealm,
      });
    } else {
      keycloakUserID = keycloakUser[0].id;
    }

    const user = await userRepo.byId(keycloakUserID!);

    if (user) {
      const orgMember = await orgRepo.getOrganizationMember({
        organizationID: authContext.organizationId,
        userID: user.id,
      });
      if (orgMember) {
        return {
          response: {
            code: EnumStatusCode.ERR_ALREADY_EXISTS,
            details: `${req.email} is already a member of this organization`,
          },
        };
      }

      const orgInvitation = await orgInvitationRepo.getPendingOrganizationInvitation({
        organizationID: authContext.organizationId,
        userID: user.id,
      });
      if (orgInvitation) {
        const userMemberships = await orgRepo.memberships({ userId: user.id });
        // if the user memberships are empty, that means the user has not logged in till now,
        // so we send the user a mail form keycloak
        if (userMemberships.length === 0) {
          await opts.keycloakClient.executeActionsEmail({
            userID: user.id,
            redirectURI: `${process.env.WEB_BASE_URL}/login?redirectURL=${process.env.WEB_BASE_URL}/account/invitations`,
            realm: opts.keycloakRealm,
          });
        } else {
          // the user has already logged in, so we send our custom org invitation email
          // eslint-disable-next-line no-lonely-if
          if (opts.mailerClient) {
            await opts.mailerClient.sendInviteEmail({
              inviteLink: `${process.env.WEB_BASE_URL}/account/invitations`,
              organizationName: organization.name,
              receiverEmail: req.email,
              invitedBy: orgInvitation.invitedBy,
            });
          }
        }

        await auditLogRepo.addAuditLog({
          organizationId: authContext.organizationId,
          auditAction: 'organization_invitation.created',
          action: 'created',
          actorId: authContext.userId,
          auditableDisplayName: req.email,
          auditableType: 'user',
          actorDisplayName: authContext.userDisplayName,
          apiKeyName: authContext.apiKeyName,
          actorType: authContext.auth === 'api_key' ? 'api_key' : 'user',
        });

        return {
          response: {
            code: EnumStatusCode.OK,
            details: 'Invited member successfully.',
          },
        };
      }
    }

    const userMemberships = await orgRepo.memberships({ userId: keycloakUserID! });
    // to verify if the user is a new user or not, we check the memberships of the user
    if (userMemberships.length > 0) {
      if (opts.mailerClient) {
        const inviter = await userRepo.byId(authContext.userId);
        await opts.mailerClient.sendInviteEmail({
          inviteLink: `${process.env.WEB_BASE_URL}/account/invitations`,
          organizationName: organization.name,
          receiverEmail: req.email,
          invitedBy: inviter?.email,
        });
      }
    } else {
      await opts.keycloakClient.executeActionsEmail({
        userID: keycloakUserID!,
        redirectURI: `${process.env.WEB_BASE_URL}/login?redirectURL=${process.env.WEB_BASE_URL}/account/invitations`,
        realm: opts.keycloakRealm,
      });
    }

    // TODO: rate limit this
    await orgInvitationRepo.inviteUser({
      email: req.email,
      userId: keycloakUserID!,
      organizationId: authContext.organizationId,
      dbUser: user,
      inviterUserId: authContext.userId,
    });

    await auditLogRepo.addAuditLog({
      organizationId: authContext.organizationId,
      auditAction: 'organization_invitation.created',
      action: 'created',
      actorId: authContext.userId,
      auditableDisplayName: req.email,
      auditableType: 'user',
      actorDisplayName: authContext.userDisplayName,
      apiKeyName: authContext.apiKeyName,
      actorType: authContext.auth === 'api_key' ? 'api_key' : 'user',
    });

    return {
      response: {
        code: EnumStatusCode.OK,
        details: 'Invited member successfully.',
      },
    };
  });
}