Skip to main content
Glama
organization.controller.ts21.6 kB
import { logger } from '@logger'; import type { ResponseWithSession } from '@middlewares/sessionAuth.middleware'; import { SessionModel } from '@models/session.model'; import { sendEmail } from '@services/email.service'; import * as organizationService from '@services/organization.service'; import * as projectService from '@services/project.service'; import * as userService from '@services/user.service'; import { type AppError, ErrorHandler } from '@utils/errors'; import type { FiltersAndPagination } from '@utils/filtersAndPagination/getFiltersAndPaginationFromBody'; import { getOrganizationFiltersAndPagination, type OrganizationFiltersParams, } from '@utils/filtersAndPagination/getOrganizationFiltersAndPagination'; import { mapOrganizationsToAPI, mapOrganizationToAPI, } from '@utils/mapper/organization'; import { hasPermission } from '@utils/permissions'; import { getPlanDetails } from '@utils/plan'; import { formatPaginatedResponse, formatResponse, type PaginatedResponse, type ResponseData, } from '@utils/responseData'; import type { NextFunction, Request } from 'express'; import { t } from 'express-intlayer'; import type { Types } from 'mongoose'; import { Stripe } from 'stripe'; import type { Organization, OrganizationAPI, OrganizationCreationData, } from '@/types/organization.types'; import type { User, UserAPI } from '@/types/user.types'; export type GetOrganizationsParams = FiltersAndPagination<OrganizationFiltersParams>; export type GetOrganizationsResult = PaginatedResponse<OrganizationAPI>; /** * Retrieves a list of organizations based on filters and pagination. */ export const getOrganizations = async ( req: Request<GetOrganizationsParams>, res: ResponseWithSession<GetOrganizationsResult>, _next: NextFunction ) => { const { user, roles } = res.locals; const { filters, sortOptions, pageSize, skip, page, getNumberOfPages } = getOrganizationFiltersAndPagination(req, res); if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } try { const organizations = await organizationService.findOrganizations( filters, skip, pageSize, sortOptions ); if ( !hasPermission( roles, 'organization:read' )({ ...res.locals, targetOrganizations: organizations, }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } const totalItems = await organizationService.countOrganizations(filters); const responseData = formatPaginatedResponse<OrganizationAPI>({ data: mapOrganizationsToAPI(organizations), page, pageSize, totalPages: getNumberOfPages(totalItems), totalItems, }); res.status(200).json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type GetOrganizationParam = { organizationId: string }; export type GetOrganizationResult = ResponseData<OrganizationAPI>; /** * Retrieves an organization by its ID. */ export const getOrganization = async ( req: Request<GetOrganizationParam, any, any>, res: ResponseWithSession<GetOrganizationResult>, _next: NextFunction ): Promise<void> => { const { roles } = res.locals; const { organizationId } = req.params as Partial<GetOrganizationParam>; if (!organizationId) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_ID_NOT_FOUND'); return; } try { const organization = await organizationService.getOrganizationById(organizationId); if ( !hasPermission( roles, 'organization:read' )({ ...res.locals, targetOrganizations: [organization], }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } const responseData = formatResponse<OrganizationAPI>({ data: mapOrganizationToAPI(organization), }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type AddOrganizationBody = OrganizationCreationData; export type AddOrganizationResult = ResponseData<OrganizationAPI>; /** * Adds a new organization to the database. */ export const addOrganization = async ( req: Request<any, any, AddOrganizationBody>, res: ResponseWithSession<AddOrganizationResult>, _next: NextFunction ): Promise<void> => { const { user } = res.locals; const organization = req.body; if (!organization) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_DATA_NOT_FOUND'); return; } if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } try { const newOrganization = await organizationService.createOrganization( organization, user.id ); const responseData = formatResponse<OrganizationAPI>({ message: t({ en: 'Organization created successfully', fr: 'Organisation créée avec succès', es: 'Organización creada con éxito', }), description: t({ en: 'Your organization has been created successfully', fr: 'Votre organisation a été créée avec succès', es: 'Su organización ha sido creada con éxito', }), data: mapOrganizationToAPI(newOrganization), }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type UpdateOrganizationBody = Partial<Organization>; export type UpdateOrganizationResult = ResponseData<OrganizationAPI>; /** * Updates an existing organization in the database. */ export const updateOrganization = async ( req: Request<undefined, undefined, UpdateOrganizationBody>, res: ResponseWithSession<UpdateOrganizationResult>, _next: NextFunction ): Promise<void> => { const { organization, roles } = res.locals; const organizationFields = req.body; if (!organizationFields) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_DATA_NOT_FOUND'); return; } if (!organization) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_DEFINED'); return; } if ( !hasPermission( roles, 'organization:write' )({ ...res.locals, targetOrganizations: [organization], }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } try { const updatedOrganization = await organizationService.updateOrganizationById( organization.id, organizationFields ); const responseData = formatResponse<OrganizationAPI>({ message: t({ en: 'Organization updated successfully', fr: 'Organisation mise à jour avec succès', es: 'Organización actualizada con éxito', }), description: t({ en: 'Your organization has been updated successfully', fr: 'Votre organisation a été mise à jour avec succès', es: 'Su organización ha sido actualizada con éxito', }), data: mapOrganizationToAPI(updatedOrganization), }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type AddOrganizationMemberBody = { userEmail: string; }; export type AddOrganizationMemberResult = ResponseData<OrganizationAPI>; /** * Add member to the organization in the database. */ export const addOrganizationMember = async ( req: Request<any, any, AddOrganizationMemberBody>, res: ResponseWithSession<AddOrganizationMemberResult>, _next: NextFunction ): Promise<void> => { const { organization, user, roles } = res.locals; const { userEmail } = req.body; if (!organization) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_DEFINED'); return; } if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } if ( !hasPermission( roles, 'organization:admin' )({ ...res.locals, targetOrganizations: [organization], }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } const planType = getPlanDetails(organization.plan); if ( planType.numberOfOrganizationUsers && organization.membersIds.length >= planType.numberOfOrganizationUsers ) { ErrorHandler.handleGenericErrorResponse(res, 'PLAN_USER_LIMIT_REACHED', { organizationId: organization.id, }); return; } try { let newMember = await userService.getUserByEmail(userEmail); if (!newMember) { // Create user if not found const newUser = await userService.createUser({ email: userEmail }); if (!newUser) { ErrorHandler.handleGenericErrorResponse(res, 'USER_CREATION_FAILED', { email: userEmail, }); return; } newMember = newUser; } const updatedOrganization = await organizationService.updateOrganizationById(organization.id, { ...organization, membersIds: [...organization.membersIds, newMember.id], }); const responseData = formatResponse<OrganizationAPI>({ message: t({ en: 'Organization updated successfully', fr: 'Organisation mise à jour avec succès', es: 'Organización actualizada con éxito', }), description: t({ en: 'Your organization has been updated successfully', fr: 'Votre organisation a été mise à jour avec succès', es: 'Su organización ha sido actualizada con éxito', }), data: mapOrganizationToAPI(updatedOrganization), }); res.json(responseData); await sendEmail({ type: 'invite', to: userEmail, username: newMember.email.slice(0, newMember.email.indexOf('@')), invitedByUsername: user.name, invitedByEmail: user.email, organizationName: organization.name, inviteLink: `${process.env.CLIENT_URL}/auth/login?email=${newMember.email}`, inviteFromIp: req.ip ?? '', inviteFromLocation: req.hostname, }); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type UpdateOrganizationMembersBody = Partial<{ membersIds: (User | UserAPI)['id'][]; adminsIds: (User | UserAPI)['id'][]; }>; export type UpdateOrganizationMembersResult = ResponseData<OrganizationAPI>; /** * Update members to the organization in the database. */ export const updateOrganizationMembers = async ( req: Request<any, any, UpdateOrganizationMembersBody>, res: ResponseWithSession<UpdateOrganizationMembersResult>, _next: NextFunction ): Promise<void> => { const { organization, roles } = res.locals; const { membersIds, adminsIds } = req.body; if (!organization) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_DEFINED'); return; } if ( !hasPermission( roles, 'organization:admin' )({ ...res.locals, targetOrganizations: [organization], }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } if (!membersIds) { ErrorHandler.handleGenericErrorResponse(res, 'INVALID_REQUEST_BODY'); return; } if (membersIds?.length === 0) { ErrorHandler.handleGenericErrorResponse( res, 'ORGANIZATION_MUST_HAVE_MEMBER' ); return; } if (adminsIds?.filter((id) => membersIds?.includes(id)).length === 0) { ErrorHandler.handleGenericErrorResponse( res, 'ORGANIZATION_MUST_HAVE_ADMIN' ); return; } try { const existingUsers = await userService.getUsersByIds(membersIds); if (!existingUsers) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_FOUND'); return; } const existingAdmins = await userService.getUsersByIds(adminsIds!); if (!existingAdmins) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_FOUND'); return; } const updatedOrganization = await organizationService.updateOrganizationById(organization.id, { membersIds: existingUsers.map((user) => user.id), adminsIds: existingAdmins.map((user) => user.id), }); const responseData = formatResponse<OrganizationAPI>({ message: t({ en: 'Organization updated successfully', fr: 'Organisation mise à jour avec succès', es: 'Organización actualizada con éxito', }), description: t({ en: 'Your organization has been updated successfully', fr: 'Votre organisation a été mise à jour avec succès', es: 'Su organización ha sido actualizada con éxito', }), data: mapOrganizationToAPI(updatedOrganization), }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type UpdateOrganizationMembersByIdParams = { organizationId: string }; export type UpdateOrganizationMembersByIdBody = Partial<{ membersIds: (User | UserAPI)['id'][]; adminsIds: (User | UserAPI)['id'][]; }>; export type UpdateOrganizationMembersByIdResult = ResponseData<OrganizationAPI>; /** * Admin-only: Update members of any organization by ID */ export const updateOrganizationMembersById = async ( req: Request< UpdateOrganizationMembersByIdParams, any, UpdateOrganizationMembersByIdBody >, res: ResponseWithSession<UpdateOrganizationMembersByIdResult>, _next: NextFunction ): Promise<void> => { const { user } = res.locals; const { organizationId } = req.params; const { membersIds, adminsIds } = req.body; if (!user) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_DEFINED'); return; } if (user.role !== 'admin') { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } if (!membersIds) { ErrorHandler.handleGenericErrorResponse(res, 'INVALID_REQUEST_BODY'); return; } if (membersIds?.length === 0) { ErrorHandler.handleGenericErrorResponse( res, 'ORGANIZATION_MUST_HAVE_MEMBER' ); return; } try { const targetOrganization = await organizationService.getOrganizationById(organizationId); if (!targetOrganization) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_FOUND'); return; } const existingUsers = await userService.getUsersByIds(membersIds); if (!existingUsers) { ErrorHandler.handleGenericErrorResponse(res, 'USER_NOT_FOUND'); return; } const finalAdminsIds = adminsIds && adminsIds.length > 0 ? adminsIds : targetOrganization.adminsIds; const existingAdmins = finalAdminsIds ? await userService.getUsersByIds(finalAdminsIds) : []; if (!existingAdmins || existingAdmins.length === 0) { ErrorHandler.handleGenericErrorResponse( res, 'ORGANIZATION_MUST_HAVE_ADMIN' ); return; } const updatedOrganization = await organizationService.updateOrganizationById(targetOrganization.id, { membersIds: existingUsers.map((user) => user.id), adminsIds: existingAdmins.map((user) => user.id), }); const responseData = formatResponse<OrganizationAPI>({ message: t({ en: 'Organization members updated successfully', fr: "Membres de l'organisation mis à jour avec succès", es: 'Miembros de la organización actualizados con éxito', }), description: t({ en: 'Organization members have been updated successfully', fr: "Les membres de l'organisation ont été mis à jour avec succès", es: 'Los miembros de la organización han sido actualizados con éxito', }), data: mapOrganizationToAPI(updatedOrganization), }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type DeleteOrganizationResult = ResponseData<OrganizationAPI>; /** * Deletes an organization from the database by its ID. */ export const deleteOrganization = async ( _req: Request, res: ResponseWithSession, _next: NextFunction ): Promise<void> => { const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!); const { organization, roles } = res.locals; if (!organization) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_DEFINED'); return; } const projects = await projectService.findProjects({ organizationId: organization.id, }); if (projects.length > 0) { ErrorHandler.handleGenericErrorResponse(res, 'PROJECTS_EXIST', { organizationId: organization.id, }); return; } if ( !hasPermission( roles, 'organization:admin' )({ ...res.locals, targetOrganizations: [organization], }) ) { ErrorHandler.handleGenericErrorResponse(res, 'PERMISSION_DENIED'); return; } try { // Cancel the subscription on Stripe if it exists if (organization.plan?.subscriptionId) { await stripe.subscriptions.cancel(organization.plan.subscriptionId); } const deletedOrganization = await organizationService.deleteOrganizationById(organization.id); if (!deletedOrganization) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_NOT_FOUND', { organizationId: organization.id, }); return; } logger.info(`Organization deleted: ${String(deletedOrganization.id)}`); const responseData = formatResponse<OrganizationAPI>({ message: t({ en: 'Organization deleted successfully', fr: 'Organisation supprimée avec succès', es: 'Organización eliminada con éxito', }), description: t({ en: 'Your organization has been deleted successfully', fr: 'Votre organisation a été supprimée avec succès', es: 'Su organización ha sido eliminada con éxito', }), data: mapOrganizationToAPI(deletedOrganization), }); // No need to update session here, as it's a delete operation res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type SelectOrganizationParam = { organizationId: string | Types.ObjectId; }; export type SelectOrganizationResult = ResponseData<OrganizationAPI>; /** * Select an organization. */ export const selectOrganization = async ( req: Request<SelectOrganizationParam>, res: ResponseWithSession<SelectOrganizationResult>, _next: NextFunction ): Promise<void> => { const { organizationId } = req.params as Partial<SelectOrganizationParam>; const { session } = res.locals; if (!organizationId) { ErrorHandler.handleGenericErrorResponse(res, 'ORGANIZATION_ID_NOT_FOUND'); return; } if (typeof session === 'undefined') { ErrorHandler.handleGenericErrorResponse(res, 'SESSION_NOT_DEFINED'); return; } try { const organization = await organizationService.getOrganizationById(organizationId); // Update session to set activeOrganizationId await SessionModel.updateOne( { _id: session.id }, { $set: { activeOrganizationId: String(organization.id), activeProjectId: null, }, } ); // No need to update session here, as it's a select operation const responseData = formatResponse<OrganizationAPI>({ message: t({ en: 'Organization retrieved successfully', fr: 'Organisation récupérée avec succès', es: 'Organización recuperada con éxito', }), description: t({ en: 'Your organization has been retrieved successfully', fr: 'Votre organisation a été récupérée avec succès', es: 'Su organización ha sido recuperada con éxito', }), data: mapOrganizationToAPI(organization), }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } }; export type UnselectOrganizationResult = ResponseData<null>; /** * Unselect an organization. */ export const unselectOrganization = async ( _req: Request, res: ResponseWithSession<UnselectOrganizationResult>, _next: NextFunction ): Promise<void> => { const { session } = res.locals; try { // Update session to clear activeOrganizationId and activeProjectId if (typeof session === 'undefined') { ErrorHandler.handleGenericErrorResponse(res, 'SESSION_NOT_DEFINED'); return; } await SessionModel.updateOne( { _id: session.id }, { $set: { activeOrganizationId: null, activeProjectId: null, }, } ); const responseData = formatResponse<null>({ message: t({ en: 'Organization unselected successfully', fr: 'Organisation désélectionnée avec succès', es: 'Organización deseleccionada con éxito', }), description: t({ en: 'Your organization has been unselected successfully', fr: 'Votre organisation a été désélectionnée avec succès', es: 'Su organización ha sido deseleccionada con éxito', }), data: null, }); res.json(responseData); return; } catch (error) { ErrorHandler.handleAppErrorResponse(res, error as AppError); return; } };

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/aymericzip/intlayer'

If you have feedback or need assistance with the MCP directory API, please join our Discord server