diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 4fd79e509..c2fa5bca8 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -5,6 +5,7 @@ import { AppService } from './app.service'; import { AttachmentsModule } from './attachments/attachments.module'; import { AuthModule } from './auth/auth.module'; import { CommentsModule } from './comments/comments.module'; +import { DevicesModule } from './devices/devices.module'; import { awsConfig } from './config/aws.config'; import { HealthModule } from './health/health.module'; import { OrganizationModule } from './organization/organization.module'; @@ -22,6 +23,7 @@ import { TasksModule } from './tasks/tasks.module'; }), AuthModule, OrganizationModule, + DevicesModule, AttachmentsModule, TasksModule, CommentsModule, diff --git a/apps/api/src/devices/devices.controller.ts b/apps/api/src/devices/devices.controller.ts new file mode 100644 index 000000000..9e8323071 --- /dev/null +++ b/apps/api/src/devices/devices.controller.ts @@ -0,0 +1,212 @@ +import { + Controller, + Get, + Param, + UseGuards +} from '@nestjs/common'; +import { + ApiHeader, + ApiOperation, + ApiParam, + ApiResponse, + ApiSecurity, + ApiTags, +} from '@nestjs/swagger'; +import { + AuthContext, + OrganizationId, +} from '../auth/auth-context.decorator'; +import { HybridAuthGuard } from '../auth/hybrid-auth.guard'; +import type { AuthContext as AuthContextType } from '../auth/types'; +import { DevicesByMemberResponseDto } from './dto/devices-by-member-response.dto'; +import { DevicesService } from './devices.service'; + +@ApiTags('Devices') +@Controller({ path: 'devices', version: '1' }) +@UseGuards(HybridAuthGuard) +@ApiSecurity('apikey') +@ApiHeader({ + name: 'X-Organization-Id', + description: + 'Organization ID (required for session auth, optional for API key auth)', + required: false, +}) +export class DevicesController { + constructor(private readonly devicesService: DevicesService) {} + + @Get() + @ApiOperation({ + summary: 'Get all devices', + description: + 'Returns all devices for the authenticated organization from FleetDM. Supports both API key authentication (X-API-Key header) and session authentication (cookies + X-Organization-Id header).', + }) + @ApiResponse({ + status: 200, + description: 'Devices retrieved successfully', + schema: { + type: 'object', + properties: { + data: { + type: 'array', + items: { $ref: '#/components/schemas/DeviceResponseDto' }, + }, + count: { + type: 'number', + description: 'Total number of devices', + example: 25, + }, + authType: { + type: 'string', + enum: ['api-key', 'session'], + description: 'How the request was authenticated', + }, + authenticatedUser: { + type: 'object', + properties: { + id: { + type: 'string', + description: 'User ID', + example: 'usr_abc123def456', + }, + email: { + type: 'string', + description: 'User email', + example: 'user@company.com', + }, + }, + }, + }, + }, + }) + @ApiResponse({ + status: 401, + description: + 'Unauthorized - Invalid authentication or insufficient permissions', + schema: { + type: 'object', + properties: { + message: { + type: 'string', + examples: [ + 'Invalid or expired API key', + 'Invalid or expired session', + 'User does not have access to organization', + 'Organization context required', + ], + }, + }, + }, + }) + @ApiResponse({ + status: 404, + description: 'Organization not found', + schema: { + type: 'object', + properties: { + message: { + type: 'string', + example: 'Organization with ID org_abc123def456 not found', + }, + }, + }, + }) + @ApiResponse({ + status: 500, + description: 'Internal server error - FleetDM integration issue', + schema: { + type: 'object', + properties: { + message: { + type: 'string', + examples: [ + 'Failed to retrieve devices: FleetDM connection failed', + 'Organization does not have FleetDM configured', + ], + }, + }, + }, + }) + async getAllDevices( + @OrganizationId() organizationId: string, + @AuthContext() authContext: AuthContextType, + ) { + const devices = await this.devicesService.findAllByOrganization(organizationId); + + return { + data: devices, + count: devices.length, + authType: authContext.authType, + ...(authContext.userId && authContext.userEmail && { + authenticatedUser: { + id: authContext.userId, + email: authContext.userEmail, + }, + }), + }; + } + + @Get('member/:memberId') + @ApiOperation({ + summary: 'Get devices by member ID', + description: + 'Returns all devices assigned to a specific member within the authenticated organization. Devices are fetched from FleetDM using the member\'s dedicated fleetDmLabelId. Supports both API key authentication (X-API-Key header) and session authentication (cookies + X-Organization-Id header).', + }) + @ApiParam({ + name: 'memberId', + description: 'Member ID to get devices for', + example: 'mem_abc123def456', + }) + @ApiResponse({ + status: 200, + description: 'Member devices retrieved successfully', + type: DevicesByMemberResponseDto, + }) + @ApiResponse({ + status: 401, + description: + 'Unauthorized - Invalid authentication or insufficient permissions', + }) + @ApiResponse({ + status: 404, + description: 'Organization or member not found', + schema: { + type: 'object', + properties: { + message: { + type: 'string', + examples: [ + 'Organization with ID org_abc123def456 not found', + 'Member with ID mem_abc123def456 not found in organization org_abc123def456', + ], + }, + }, + }, + }) + @ApiResponse({ + status: 500, + description: 'Internal server error - FleetDM integration issue', + }) + async getDevicesByMember( + @Param('memberId') memberId: string, + @OrganizationId() organizationId: string, + @AuthContext() authContext: AuthContextType, + ): Promise { + const [devices, member] = await Promise.all([ + this.devicesService.findAllByMember(organizationId, memberId), + this.devicesService.getMemberById(organizationId, memberId), + ]); + + return { + data: devices, + count: devices.length, + member, + authType: authContext.authType, + ...(authContext.userId && authContext.userEmail && { + authenticatedUser: { + id: authContext.userId, + email: authContext.userEmail, + }, + }), + }; + } +} diff --git a/apps/api/src/devices/devices.module.ts b/apps/api/src/devices/devices.module.ts new file mode 100644 index 000000000..5ecf760a2 --- /dev/null +++ b/apps/api/src/devices/devices.module.ts @@ -0,0 +1,13 @@ +import { Module } from '@nestjs/common'; +import { AuthModule } from '../auth/auth.module'; +import { FleetService } from '../lib/fleet.service'; +import { DevicesController } from './devices.controller'; +import { DevicesService } from './devices.service'; + +@Module({ + imports: [AuthModule], + controllers: [DevicesController], + providers: [DevicesService, FleetService], + exports: [DevicesService, FleetService], +}) +export class DevicesModule {} diff --git a/apps/api/src/devices/devices.service.ts b/apps/api/src/devices/devices.service.ts new file mode 100644 index 000000000..5519c24ad --- /dev/null +++ b/apps/api/src/devices/devices.service.ts @@ -0,0 +1,160 @@ +import { Injectable, NotFoundException, Logger } from '@nestjs/common'; +import { db } from '@trycompai/db'; +import { FleetService } from '../lib/fleet.service'; +import type { DeviceResponseDto } from './dto/device-responses.dto'; +import type { MemberResponseDto } from './dto/member-responses.dto'; + +@Injectable() +export class DevicesService { + private readonly logger = new Logger(DevicesService.name); + + constructor(private readonly fleetService: FleetService) {} + + async findAllByOrganization(organizationId: string): Promise { + try { + // Get organization and its FleetDM label ID + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + select: { + id: true, + name: true, + fleetDmLabelId: true + }, + }); + + if (!organization) { + throw new NotFoundException(`Organization with ID ${organizationId} not found`); + } + + if (!organization.fleetDmLabelId) { + this.logger.warn(`Organization ${organizationId} does not have FleetDM label configured`); + return []; + } + + // Get all hosts for the organization's label + const labelHosts = await this.fleetService.getHostsByLabel(organization.fleetDmLabelId); + + if (!labelHosts.hosts || labelHosts.hosts.length === 0) { + this.logger.log(`No devices found for organization ${organizationId}`); + return []; + } + + // Extract host IDs + const hostIds = labelHosts.hosts.map((host: { id: number }) => host.id); + this.logger.log(`Found ${hostIds.length} devices for organization ${organizationId}`); + + // Get detailed information for each host + const devices = await this.fleetService.getMultipleHosts(hostIds); + + this.logger.log(`Retrieved ${devices.length} device details for organization ${organizationId}`); + return devices; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Failed to retrieve devices for organization ${organizationId}:`, error); + throw new Error(`Failed to retrieve devices: ${error.message}`); + } + } + + async findAllByMember(organizationId: string, memberId: string): Promise { + try { + // First verify the organization exists + const organization = await db.organization.findUnique({ + where: { id: organizationId }, + select: { + id: true, + name: true + }, + }); + + if (!organization) { + throw new NotFoundException(`Organization with ID ${organizationId} not found`); + } + + // Verify the member exists and belongs to the organization + const member = await db.member.findFirst({ + where: { + id: memberId, + organizationId: organizationId, + }, + select: { + id: true, + userId: true, + role: true, + department: true, + isActive: true, + fleetDmLabelId: true, + organizationId: true, + createdAt: true, + }, + }); + + if (!member) { + throw new NotFoundException(`Member with ID ${memberId} not found in organization ${organizationId}`); + } + + if (!member.fleetDmLabelId) { + this.logger.warn(`Member ${memberId} does not have FleetDM label configured`); + return []; + } + + // Get devices for the member's specific FleetDM label + const labelHosts = await this.fleetService.getHostsByLabel(member.fleetDmLabelId); + + if (!labelHosts.hosts || labelHosts.hosts.length === 0) { + this.logger.log(`No devices found for member ${memberId}`); + return []; + } + + // Extract host IDs + const hostIds = labelHosts.hosts.map((host: { id: number }) => host.id); + this.logger.log(`Found ${hostIds.length} devices for member ${memberId}`); + + // Get detailed information for each host + const devices = await this.fleetService.getMultipleHosts(hostIds); + + this.logger.log(`Retrieved ${devices.length} device details for member ${memberId} in organization ${organizationId}`); + return devices; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Failed to retrieve devices for member ${memberId} in organization ${organizationId}:`, error); + throw new Error(`Failed to retrieve member devices: ${error.message}`); + } + } + + async getMemberById(organizationId: string, memberId: string): Promise { + try { + const member = await db.member.findFirst({ + where: { + id: memberId, + organizationId: organizationId, + }, + select: { + id: true, + userId: true, + role: true, + department: true, + isActive: true, + fleetDmLabelId: true, + organizationId: true, + createdAt: true, + }, + }); + + if (!member) { + throw new NotFoundException(`Member with ID ${memberId} not found in organization ${organizationId}`); + } + + return member; + } catch (error) { + if (error instanceof NotFoundException) { + throw error; + } + this.logger.error(`Failed to retrieve member ${memberId} in organization ${organizationId}:`, error); + throw new Error(`Failed to retrieve member: ${error.message}`); + } + } +} diff --git a/apps/api/src/devices/dto/device-responses.dto.ts b/apps/api/src/devices/dto/device-responses.dto.ts new file mode 100644 index 000000000..c6609e248 --- /dev/null +++ b/apps/api/src/devices/dto/device-responses.dto.ts @@ -0,0 +1,244 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class FleetPolicyDto { + @ApiProperty({ description: 'Policy ID', example: 123 }) + id: number; + + @ApiProperty({ description: 'Policy name', example: 'Password Policy' }) + name: string; + + @ApiProperty({ description: 'Policy query', example: 'SELECT * FROM users;' }) + query: string; + + @ApiProperty({ description: 'Whether policy is critical', example: true }) + critical: boolean; + + @ApiProperty({ description: 'Policy description', example: 'Ensures strong passwords' }) + description: string; + + @ApiProperty({ description: 'Author ID', example: 456 }) + author_id: number; + + @ApiProperty({ description: 'Author name', example: 'John Doe' }) + author_name: string; + + @ApiProperty({ description: 'Author email', example: 'john@example.com' }) + author_email: string; + + @ApiProperty({ description: 'Team ID', example: 789, nullable: true }) + team_id: number | null; + + @ApiProperty({ description: 'Policy resolution', example: 'Update password settings' }) + resolution: string; + + @ApiProperty({ description: 'Platform', example: 'darwin' }) + platform: string; + + @ApiProperty({ description: 'Calendar events enabled', example: false }) + calendar_events_enabled: boolean; + + @ApiProperty({ description: 'Created at', example: '2024-01-01T00:00:00Z' }) + created_at: string; + + @ApiProperty({ description: 'Updated at', example: '2024-01-15T00:00:00Z' }) + updated_at: string; + + @ApiProperty({ description: 'Policy response', example: 'compliant' }) + response: string; +} + +export class DeviceResponseDto { + @ApiProperty({ description: 'Device created at', example: '2024-01-01T00:00:00Z' }) + created_at: string; + + @ApiProperty({ description: 'Device updated at', example: '2024-01-15T00:00:00Z' }) + updated_at: string; + + @ApiProperty({ description: 'Software list', type: 'array', items: { type: 'object' } }) + software: object[]; + + @ApiProperty({ description: 'Software updated at', example: '2024-01-10T00:00:00Z' }) + software_updated_at: string; + + @ApiProperty({ description: 'Device ID', example: 123 }) + id: number; + + @ApiProperty({ description: 'Detail updated at', example: '2024-01-10T00:00:00Z' }) + detail_updated_at: string; + + @ApiProperty({ description: 'Label updated at', example: '2024-01-10T00:00:00Z' }) + label_updated_at: string; + + @ApiProperty({ description: 'Policy updated at', example: '2024-01-10T00:00:00Z' }) + policy_updated_at: string; + + @ApiProperty({ description: 'Last enrolled at', example: '2024-01-01T00:00:00Z' }) + last_enrolled_at: string; + + @ApiProperty({ description: 'Last seen time', example: '2024-01-15T12:00:00Z' }) + seen_time: string; + + @ApiProperty({ description: 'Refetch requested', example: false }) + refetch_requested: boolean; + + @ApiProperty({ description: 'Hostname', example: 'johns-macbook' }) + hostname: string; + + @ApiProperty({ description: 'Device UUID', example: 'abc123def456' }) + uuid: string; + + @ApiProperty({ description: 'Platform', example: 'darwin' }) + platform: string; + + @ApiProperty({ description: 'Osquery version', example: '5.10.2' }) + osquery_version: string; + + @ApiProperty({ description: 'Orbit version', example: '1.19.0' }) + orbit_version: string; + + @ApiProperty({ description: 'Fleet desktop version', example: '1.19.0' }) + fleet_desktop_version: string; + + @ApiProperty({ description: 'Scripts enabled', example: true }) + scripts_enabled: boolean; + + @ApiProperty({ description: 'OS version', example: 'macOS 14.2.1' }) + os_version: string; + + @ApiProperty({ description: 'Build', example: '23C71' }) + build: string; + + @ApiProperty({ description: 'Platform like', example: 'darwin' }) + platform_like: string; + + @ApiProperty({ description: 'Code name', example: 'sonoma' }) + code_name: string; + + @ApiProperty({ description: 'Uptime in seconds', example: 86400 }) + uptime: number; + + @ApiProperty({ description: 'Memory in bytes', example: 17179869184 }) + memory: number; + + @ApiProperty({ description: 'CPU type', example: 'x86_64' }) + cpu_type: string; + + @ApiProperty({ description: 'CPU subtype', example: 'x86_64h' }) + cpu_subtype: string; + + @ApiProperty({ description: 'CPU brand', example: 'Intel(R) Core(TM) i7-9750H' }) + cpu_brand: string; + + @ApiProperty({ description: 'CPU physical cores', example: 6 }) + cpu_physical_cores: number; + + @ApiProperty({ description: 'CPU logical cores', example: 12 }) + cpu_logical_cores: number; + + @ApiProperty({ description: 'Hardware vendor', example: 'Apple Inc.' }) + hardware_vendor: string; + + @ApiProperty({ description: 'Hardware model', example: 'MacBookPro16,1' }) + hardware_model: string; + + @ApiProperty({ description: 'Hardware version', example: '1.0' }) + hardware_version: string; + + @ApiProperty({ description: 'Hardware serial', example: 'C02XW0AAJGH6' }) + hardware_serial: string; + + @ApiProperty({ description: 'Computer name', example: "John's MacBook Pro" }) + computer_name: string; + + @ApiProperty({ description: 'Public IP', example: '203.0.113.1' }) + public_ip: string; + + @ApiProperty({ description: 'Primary IP', example: '192.168.1.100' }) + primary_ip: string; + + @ApiProperty({ description: 'Primary MAC', example: '00:11:22:33:44:55' }) + primary_mac: string; + + @ApiProperty({ description: 'Distributed interval', example: 10 }) + distributed_interval: number; + + @ApiProperty({ description: 'Config TLS refresh', example: 3600 }) + config_tls_refresh: number; + + @ApiProperty({ description: 'Logger TLS period', example: 300 }) + logger_tls_period: number; + + @ApiProperty({ description: 'Team ID', example: 1, nullable: true }) + team_id: number | null; + + @ApiProperty({ description: 'Pack stats', type: 'array', items: { type: 'object' } }) + pack_stats: object[]; + + @ApiProperty({ description: 'Team name', example: 'Engineering', nullable: true }) + team_name: string | null; + + @ApiProperty({ description: 'Users', type: 'array', items: { type: 'object' } }) + users: object[]; + + @ApiProperty({ description: 'Disk space available in GB', example: 250.5 }) + gigs_disk_space_available: number; + + @ApiProperty({ description: 'Percent disk space available', example: 75.2 }) + percent_disk_space_available: number; + + @ApiProperty({ description: 'Total disk space in GB', example: 500.0 }) + gigs_total_disk_space: number; + + @ApiProperty({ description: 'Disk encryption enabled', example: true }) + disk_encryption_enabled: boolean; + + @ApiProperty({ + description: 'Issues', + type: 'object', + additionalProperties: true, + }) + issues: Record; + + @ApiProperty({ + description: 'MDM info', + type: 'object', + additionalProperties: true, + }) + mdm: Record; + + @ApiProperty({ description: 'Refetch critical queries until', example: '2024-01-20T00:00:00Z', nullable: true }) + refetch_critical_queries_until: string | null; + + @ApiProperty({ description: 'Last restarted at', example: '2024-01-10T08:00:00Z' }) + last_restarted_at: string; + + @ApiProperty({ description: 'Policies', type: [FleetPolicyDto] }) + policies: FleetPolicyDto[]; + + @ApiProperty({ description: 'Labels', type: 'array', items: { type: 'object' } }) + labels: object[]; + + @ApiProperty({ description: 'Packs', type: 'array', items: { type: 'object' } }) + packs: object[]; + + @ApiProperty({ description: 'Batteries', type: 'array', items: { type: 'object' } }) + batteries: object[]; + + @ApiProperty({ description: 'End users', type: 'array', items: { type: 'object' } }) + end_users: object[]; + + @ApiProperty({ description: 'Last MDM enrolled at', example: '2024-01-01T00:00:00Z' }) + last_mdm_enrolled_at: string; + + @ApiProperty({ description: 'Last MDM checked in at', example: '2024-01-15T12:00:00Z' }) + last_mdm_checked_in_at: string; + + @ApiProperty({ description: 'Device status', example: 'online' }) + status: string; + + @ApiProperty({ description: 'Display text', example: 'Johns MacBook Pro' }) + display_text: string; + + @ApiProperty({ description: 'Display name', example: "John's MacBook Pro" }) + display_name: string; +} diff --git a/apps/api/src/devices/dto/devices-by-member-response.dto.ts b/apps/api/src/devices/dto/devices-by-member-response.dto.ts new file mode 100644 index 000000000..9863f6d02 --- /dev/null +++ b/apps/api/src/devices/dto/devices-by-member-response.dto.ts @@ -0,0 +1,43 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { DeviceResponseDto } from './device-responses.dto'; +import { MemberResponseDto } from './member-responses.dto'; + +export class DevicesByMemberResponseDto { + @ApiProperty({ + description: 'Array of devices assigned to the member', + type: [DeviceResponseDto], + }) + data: DeviceResponseDto[]; + + @ApiProperty({ + description: 'Total number of devices for this member', + example: 3, + }) + count: number; + + @ApiProperty({ + description: 'Member information', + type: MemberResponseDto, + }) + member: MemberResponseDto; + + @ApiProperty({ + description: 'How the request was authenticated', + enum: ['api-key', 'session'], + example: 'api-key', + }) + authType: string; + + @ApiProperty({ + description: 'Authenticated user information (present for session auth)', + required: false, + example: { + id: 'usr_abc123def456', + email: 'user@company.com', + }, + }) + authenticatedUser?: { + id: string; + email: string; + }; +} diff --git a/apps/api/src/devices/dto/member-responses.dto.ts b/apps/api/src/devices/dto/member-responses.dto.ts new file mode 100644 index 000000000..9be66ad26 --- /dev/null +++ b/apps/api/src/devices/dto/member-responses.dto.ts @@ -0,0 +1,53 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class MemberResponseDto { + @ApiProperty({ + description: 'Member ID', + example: 'mem_abc123def456', + }) + id: string; + + @ApiProperty({ + description: 'User ID associated with member', + example: 'usr_abc123def456', + }) + userId: string; + + @ApiProperty({ + description: 'Member role', + example: 'admin', + }) + role: string; + + @ApiProperty({ + description: 'Member department', + example: 'engineering', + nullable: true, + }) + department: string | null; + + @ApiProperty({ + description: 'Whether member is active', + example: true, + }) + isActive: boolean; + + @ApiProperty({ + description: 'FleetDM label ID for member devices', + example: 123, + nullable: true, + }) + fleetDmLabelId: number | null; + + @ApiProperty({ + description: 'Organization ID this member belongs to', + example: 'org_abc123def456', + }) + organizationId: string; + + @ApiProperty({ + description: 'When the member was created', + example: '2024-01-01T00:00:00Z', + }) + createdAt: Date; +} diff --git a/apps/api/src/lib/fleet.service.ts b/apps/api/src/lib/fleet.service.ts new file mode 100644 index 000000000..e3d336454 --- /dev/null +++ b/apps/api/src/lib/fleet.service.ts @@ -0,0 +1,73 @@ +import { Injectable, Logger } from '@nestjs/common'; +import axios, { AxiosInstance } from 'axios'; + +@Injectable() +export class FleetService { + private readonly logger = new Logger(FleetService.name); + private fleetInstance: AxiosInstance; + + constructor() { + this.fleetInstance = axios.create({ + baseURL: `${process.env.FLEET_URL}/api/v1/fleet`, + headers: { + Authorization: `Bearer ${process.env.FLEET_TOKEN}`, + 'Content-Type': 'application/json', + }, + timeout: 30000, // 30 second timeout + }); + + // Add request/response interceptors for logging + this.fleetInstance.interceptors.request.use( + (config) => { + this.logger.debug(`FleetDM Request: ${config.method?.toUpperCase()} ${config.url}`); + return config; + }, + (error) => { + this.logger.error('FleetDM Request Error:', error); + return Promise.reject(error); + } + ); + + this.fleetInstance.interceptors.response.use( + (response) => { + this.logger.debug(`FleetDM Response: ${response.status} ${response.config.url}`); + return response; + }, + (error) => { + this.logger.error(`FleetDM Response Error: ${error.response?.status} ${error.config?.url}`, error.response?.data); + return Promise.reject(error); + } + ); + } + + async getHostsByLabel(labelId: number) { + try { + const response = await this.fleetInstance.get(`/labels/${labelId}/hosts`); + return response.data; + } catch (error) { + this.logger.error(`Failed to get hosts for label ${labelId}:`, error); + throw new Error(`Failed to fetch hosts for label ${labelId}`); + } + } + + async getHostById(hostId: number) { + try { + const response = await this.fleetInstance.get(`/hosts/${hostId}`); + return response.data; + } catch (error) { + this.logger.error(`Failed to get host ${hostId}:`, error); + throw new Error(`Failed to fetch host ${hostId}`); + } + } + + async getMultipleHosts(hostIds: number[]) { + try { + const requests = hostIds.map(id => this.getHostById(id)); + const responses = await Promise.all(requests); + return responses.map(response => response.host); + } catch (error) { + this.logger.error('Failed to get multiple hosts:', error); + throw new Error('Failed to fetch multiple hosts'); + } + } +} diff --git a/apps/app/src/utils/auth.ts b/apps/app/src/utils/auth.ts index 5f133ea83..a66f6da1f 100644 --- a/apps/app/src/utils/auth.ts +++ b/apps/app/src/utils/auth.ts @@ -44,7 +44,9 @@ export const auth = betterAuth({ provider: 'postgresql', }), baseURL: process.env.NEXT_PUBLIC_BETTER_AUTH_URL, - trustedOrigins: process.env.AUTH_TRUSTED_ORIGINS ? process.env.AUTH_TRUSTED_ORIGINS.split(",").map(o => o.trim()) : ['http://localhost:3000', 'https://*.trycomp.ai', 'http://localhost:3002'], + trustedOrigins: process.env.AUTH_TRUSTED_ORIGINS + ? process.env.AUTH_TRUSTED_ORIGINS.split(',').map((o) => o.trim()) + : ['http://localhost:3000', 'https://*.trycomp.ai', 'http://localhost:3002'], emailAndPassword: { enabled: true, }, @@ -109,6 +111,7 @@ export const auth = betterAuth({ secret: process.env.AUTH_SECRET!, plugins: [ organization({ + membershipLimit: 100000000000, async sendInvitationEmail(data) { const isLocalhost = process.env.NODE_ENV === 'development'; const protocol = isLocalhost ? 'http' : 'https'; diff --git a/apps/portal/src/app/lib/auth.ts b/apps/portal/src/app/lib/auth.ts index 22a7fc74e..b7eadfcb3 100644 --- a/apps/portal/src/app/lib/auth.ts +++ b/apps/portal/src/app/lib/auth.ts @@ -19,6 +19,7 @@ export const auth = betterAuth({ secret: process.env.AUTH_SECRET!, plugins: [ organization({ + membershipLimit: 100000000000, async sendInvitationEmail(data) { console.log( 'process.env.NEXT_PUBLIC_BETTER_AUTH_URL',