feat: add trip invitation creation and acceptance flow
This commit is contained in:
@@ -0,0 +1,78 @@
|
|||||||
|
import { TripInvitationsController } from './trip-invitations.controller';
|
||||||
|
import type { AuthenticatedUser } from '../../../../libs/auth/src';
|
||||||
|
|
||||||
|
describe('TripInvitationsController', () => {
|
||||||
|
const currentUser: AuthenticatedUser = {
|
||||||
|
id: 'owner-1',
|
||||||
|
externalSubjectId: 'sub-1',
|
||||||
|
displayName: 'Owner',
|
||||||
|
email: 'owner@example.com',
|
||||||
|
};
|
||||||
|
|
||||||
|
it('POST /trips/:tripId/invitations creates an invitation and returns the raw token', async () => {
|
||||||
|
const result = {
|
||||||
|
invitation: { id: 'inv-1', tripId: 't1', email: 'friend@example.com' },
|
||||||
|
rawToken: 'raw-token',
|
||||||
|
};
|
||||||
|
const invitationsService = {
|
||||||
|
createInvitation: jest.fn().mockResolvedValue(result),
|
||||||
|
};
|
||||||
|
const controller = new TripInvitationsController(
|
||||||
|
invitationsService as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
controller.create('t1', currentUser, { email: 'friend@example.com' }),
|
||||||
|
).resolves.toEqual(result);
|
||||||
|
expect(invitationsService.createInvitation).toHaveBeenCalledWith(
|
||||||
|
't1',
|
||||||
|
'owner-1',
|
||||||
|
'friend@example.com',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('GET /trips/:tripId/invitations lists invitations', async () => {
|
||||||
|
const invitationsService = {
|
||||||
|
listInvitations: jest.fn().mockResolvedValue([]),
|
||||||
|
};
|
||||||
|
const controller = new TripInvitationsController(
|
||||||
|
invitationsService as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(controller.list('t1')).resolves.toEqual([]);
|
||||||
|
expect(invitationsService.listInvitations).toHaveBeenCalledWith('t1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('DELETE /trips/:tripId/invitations/:invitationId removes an invitation', async () => {
|
||||||
|
const invitationsService = {
|
||||||
|
removeInvitation: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const controller = new TripInvitationsController(
|
||||||
|
invitationsService as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
await controller.remove('t1', 'inv-1');
|
||||||
|
expect(invitationsService.removeInvitation).toHaveBeenCalledWith(
|
||||||
|
't1',
|
||||||
|
'inv-1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('POST /invitations/:token/accept accepts the invitation for the current user', async () => {
|
||||||
|
const member = { id: 'm1', tripId: 't1', userId: 'owner-1' };
|
||||||
|
const invitationsService = {
|
||||||
|
acceptInvitation: jest.fn().mockResolvedValue(member),
|
||||||
|
};
|
||||||
|
const controller = new TripInvitationsController(
|
||||||
|
invitationsService as never,
|
||||||
|
);
|
||||||
|
|
||||||
|
await expect(controller.accept('raw-token', currentUser)).resolves.toEqual(
|
||||||
|
member,
|
||||||
|
);
|
||||||
|
expect(invitationsService.acceptInvitation).toHaveBeenCalledWith(
|
||||||
|
'raw-token',
|
||||||
|
'owner-1',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
68
backend/apps/api/src/trips/trip-invitations.controller.ts
Normal file
68
backend/apps/api/src/trips/trip-invitations.controller.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Controller,
|
||||||
|
Delete,
|
||||||
|
Get,
|
||||||
|
Param,
|
||||||
|
Post,
|
||||||
|
UseGuards,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { OidcAuthGuard } from '../../../../libs/auth/src';
|
||||||
|
import type { AuthenticatedUser } from '../../../../libs/auth/src';
|
||||||
|
import {
|
||||||
|
TripInvitationsService,
|
||||||
|
TripMembershipGuard,
|
||||||
|
TripRoles,
|
||||||
|
} from '../../../../libs/trips/src';
|
||||||
|
import type { TripInvitation, TripMember } from '../../../../libs/trips/src';
|
||||||
|
import { CurrentUser } from '../auth/current-user.decorator';
|
||||||
|
|
||||||
|
interface CreateTripInvitationDto {
|
||||||
|
email: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Controller()
|
||||||
|
export class TripInvitationsController {
|
||||||
|
constructor(private readonly invitationsService: TripInvitationsService) {}
|
||||||
|
|
||||||
|
@Post('trips/:tripId/invitations')
|
||||||
|
@UseGuards(OidcAuthGuard, TripMembershipGuard)
|
||||||
|
@TripRoles('OWNER')
|
||||||
|
create(
|
||||||
|
@Param('tripId') tripId: string,
|
||||||
|
@CurrentUser() currentUser: AuthenticatedUser,
|
||||||
|
@Body() dto: CreateTripInvitationDto,
|
||||||
|
): Promise<{ invitation: TripInvitation; rawToken: string }> {
|
||||||
|
return this.invitationsService.createInvitation(
|
||||||
|
tripId,
|
||||||
|
currentUser.id,
|
||||||
|
dto.email,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Get('trips/:tripId/invitations')
|
||||||
|
@UseGuards(OidcAuthGuard, TripMembershipGuard)
|
||||||
|
@TripRoles('OWNER')
|
||||||
|
list(@Param('tripId') tripId: string): Promise<TripInvitation[]> {
|
||||||
|
return this.invitationsService.listInvitations(tripId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Delete('trips/:tripId/invitations/:invitationId')
|
||||||
|
@UseGuards(OidcAuthGuard, TripMembershipGuard)
|
||||||
|
@TripRoles('OWNER')
|
||||||
|
remove(
|
||||||
|
@Param('tripId') tripId: string,
|
||||||
|
@Param('invitationId') invitationId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.invitationsService.removeInvitation(tripId, invitationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Post('invitations/:token/accept')
|
||||||
|
@UseGuards(OidcAuthGuard)
|
||||||
|
accept(
|
||||||
|
@Param('token') token: string,
|
||||||
|
@CurrentUser() currentUser: AuthenticatedUser,
|
||||||
|
): Promise<TripMember> {
|
||||||
|
return this.invitationsService.acceptInvitation(token, currentUser.id);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,9 +3,14 @@ import { AuthModule } from '../../../../libs/auth/src';
|
|||||||
import { TripsLibModule } from '../../../../libs/trips/src';
|
import { TripsLibModule } from '../../../../libs/trips/src';
|
||||||
import { TripsController } from './trips.controller';
|
import { TripsController } from './trips.controller';
|
||||||
import { TripMembersController } from './trip-members.controller';
|
import { TripMembersController } from './trip-members.controller';
|
||||||
|
import { TripInvitationsController } from './trip-invitations.controller';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [AuthModule, TripsLibModule],
|
imports: [AuthModule, TripsLibModule],
|
||||||
controllers: [TripsController, TripMembersController],
|
controllers: [
|
||||||
|
TripsController,
|
||||||
|
TripMembersController,
|
||||||
|
TripInvitationsController,
|
||||||
|
],
|
||||||
})
|
})
|
||||||
export class TripsApiModule {}
|
export class TripsApiModule {}
|
||||||
|
|||||||
@@ -3,5 +3,6 @@ export * from './trips.service';
|
|||||||
export * from './trip-settings.service';
|
export * from './trip-settings.service';
|
||||||
export * from './trip-members.service';
|
export * from './trip-members.service';
|
||||||
export * from './trip-membership.guard';
|
export * from './trip-membership.guard';
|
||||||
|
export * from './trip-invitations.service';
|
||||||
export * from './trip-roles.decorator';
|
export * from './trip-roles.decorator';
|
||||||
export * from './trips.module';
|
export * from './trips.module';
|
||||||
|
|||||||
91
backend/libs/trips/src/trip-invitations.repository.ts
Normal file
91
backend/libs/trips/src/trip-invitations.repository.ts
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
|
import type { Kysely } from 'kysely';
|
||||||
|
import { KYSELY_DB } from '../../database/src';
|
||||||
|
import type { Database } from '../../database/src';
|
||||||
|
import type { CreateTripInvitationFields, TripInvitation } from './trip.types';
|
||||||
|
|
||||||
|
function toTripInvitation(row: {
|
||||||
|
id: string;
|
||||||
|
trip_id: string;
|
||||||
|
email: string;
|
||||||
|
invited_by_user_id: string;
|
||||||
|
token_hash: string;
|
||||||
|
expires_at: Date;
|
||||||
|
accepted_at: Date | null;
|
||||||
|
created_at: Date;
|
||||||
|
updated_at: Date;
|
||||||
|
}): TripInvitation {
|
||||||
|
return {
|
||||||
|
id: row.id,
|
||||||
|
tripId: row.trip_id,
|
||||||
|
email: row.email,
|
||||||
|
invitedByUserId: row.invited_by_user_id,
|
||||||
|
tokenHash: row.token_hash,
|
||||||
|
expiresAt: row.expires_at,
|
||||||
|
acceptedAt: row.accepted_at,
|
||||||
|
createdAt: row.created_at,
|
||||||
|
updatedAt: row.updated_at,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TripInvitationsRepository {
|
||||||
|
constructor(@Inject(KYSELY_DB) private readonly db: Kysely<Database>) {}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
tripId: string,
|
||||||
|
fields: CreateTripInvitationFields,
|
||||||
|
): Promise<TripInvitation> {
|
||||||
|
const row = await this.db
|
||||||
|
.insertInto('trip_invitations')
|
||||||
|
.values({
|
||||||
|
trip_id: tripId,
|
||||||
|
email: fields.email,
|
||||||
|
invited_by_user_id: fields.invitedByUserId,
|
||||||
|
token_hash: fields.tokenHash,
|
||||||
|
expires_at: fields.expiresAt.toISOString(),
|
||||||
|
})
|
||||||
|
.returningAll()
|
||||||
|
.executeTakeFirstOrThrow();
|
||||||
|
return toTripInvitation(row);
|
||||||
|
}
|
||||||
|
|
||||||
|
async findByTokenHash(
|
||||||
|
tokenHash: string,
|
||||||
|
): Promise<TripInvitation | undefined> {
|
||||||
|
const row = await this.db
|
||||||
|
.selectFrom('trip_invitations')
|
||||||
|
.selectAll()
|
||||||
|
.where('token_hash', '=', tokenHash)
|
||||||
|
.executeTakeFirst();
|
||||||
|
return row ? toTripInvitation(row) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
async listByTrip(tripId: string): Promise<TripInvitation[]> {
|
||||||
|
const rows = await this.db
|
||||||
|
.selectFrom('trip_invitations')
|
||||||
|
.selectAll()
|
||||||
|
.where('trip_id', '=', tripId)
|
||||||
|
.execute();
|
||||||
|
return rows.map(toTripInvitation);
|
||||||
|
}
|
||||||
|
|
||||||
|
async markAccepted(invitationId: string): Promise<void> {
|
||||||
|
await this.db
|
||||||
|
.updateTable('trip_invitations')
|
||||||
|
.set({
|
||||||
|
accepted_at: new Date().toISOString(),
|
||||||
|
updated_at: new Date().toISOString(),
|
||||||
|
})
|
||||||
|
.where('id', '=', invitationId)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(tripId: string, invitationId: string): Promise<void> {
|
||||||
|
await this.db
|
||||||
|
.deleteFrom('trip_invitations')
|
||||||
|
.where('trip_id', '=', tripId)
|
||||||
|
.where('id', '=', invitationId)
|
||||||
|
.execute();
|
||||||
|
}
|
||||||
|
}
|
||||||
119
backend/libs/trips/src/trip-invitations.service.spec.ts
Normal file
119
backend/libs/trips/src/trip-invitations.service.spec.ts
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { TripInvitationsService } from './trip-invitations.service';
|
||||||
|
|
||||||
|
describe('TripInvitationsService.acceptInvitation', () => {
|
||||||
|
const future = new Date(Date.now() + 60_000);
|
||||||
|
const past = new Date(Date.now() - 60_000);
|
||||||
|
|
||||||
|
it('rejects accepting the same invitation twice', async () => {
|
||||||
|
const repo = {
|
||||||
|
findByTokenHash: jest.fn().mockResolvedValue({
|
||||||
|
id: 'inv-1',
|
||||||
|
tripId: 't1',
|
||||||
|
expiresAt: future,
|
||||||
|
acceptedAt: new Date(),
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const members = { upsertActiveMember: jest.fn() };
|
||||||
|
const service = new TripInvitationsService(repo as never, members as never);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.acceptInvitation('raw-token', 'user-3'),
|
||||||
|
).rejects.toThrow(ConflictException);
|
||||||
|
expect(members.upsertActiveMember).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an expired invitation', async () => {
|
||||||
|
const repo = {
|
||||||
|
findByTokenHash: jest.fn().mockResolvedValue({
|
||||||
|
id: 'inv-1',
|
||||||
|
tripId: 't1',
|
||||||
|
expiresAt: past,
|
||||||
|
acceptedAt: null,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
const members = { upsertActiveMember: jest.fn() };
|
||||||
|
const service = new TripInvitationsService(repo as never, members as never);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.acceptInvitation('raw-token', 'user-2'),
|
||||||
|
).rejects.toThrow(BadRequestException);
|
||||||
|
expect(members.upsertActiveMember).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('rejects an unknown token without leaking whether the trip exists', async () => {
|
||||||
|
const repo = { findByTokenHash: jest.fn().mockResolvedValue(undefined) };
|
||||||
|
const members = { upsertActiveMember: jest.fn() };
|
||||||
|
const service = new TripInvitationsService(repo as never, members as never);
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
service.acceptInvitation('does-not-exist', 'user-2'),
|
||||||
|
).rejects.toThrow(NotFoundException);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('accepts a fresh, unexpired invitation and activates trip membership', async () => {
|
||||||
|
const repo = {
|
||||||
|
findByTokenHash: jest.fn().mockResolvedValue({
|
||||||
|
id: 'inv-1',
|
||||||
|
tripId: 't1',
|
||||||
|
expiresAt: future,
|
||||||
|
acceptedAt: null,
|
||||||
|
}),
|
||||||
|
markAccepted: jest.fn().mockResolvedValue(undefined),
|
||||||
|
};
|
||||||
|
const members = {
|
||||||
|
upsertActiveMember: jest
|
||||||
|
.fn()
|
||||||
|
.mockResolvedValue({ id: 'm1', tripId: 't1', userId: 'user-2' }),
|
||||||
|
};
|
||||||
|
const service = new TripInvitationsService(repo as never, members as never);
|
||||||
|
|
||||||
|
const member = await service.acceptInvitation('raw-token', 'user-2');
|
||||||
|
|
||||||
|
expect(repo.markAccepted).toHaveBeenCalledWith('inv-1');
|
||||||
|
expect(members.upsertActiveMember).toHaveBeenCalledWith(
|
||||||
|
't1',
|
||||||
|
'user-2',
|
||||||
|
'MEMBER',
|
||||||
|
);
|
||||||
|
expect(member).toEqual({ id: 'm1', tripId: 't1', userId: 'user-2' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('TripInvitationsService.createInvitation', () => {
|
||||||
|
it('stores only a hash of the generated token and returns the raw token once', async () => {
|
||||||
|
const repo = {
|
||||||
|
create: jest
|
||||||
|
.fn()
|
||||||
|
.mockImplementation((tripId: string, fields: Record<string, unknown>) =>
|
||||||
|
Promise.resolve({ id: 'inv-1', tripId, ...fields }),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
const members = { upsertActiveMember: jest.fn() };
|
||||||
|
const service = new TripInvitationsService(repo as never, members as never);
|
||||||
|
|
||||||
|
const result = await service.createInvitation(
|
||||||
|
't1',
|
||||||
|
'owner-1',
|
||||||
|
'friend@example.com',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result.rawToken).toEqual(expect.any(String));
|
||||||
|
expect(repo.create).toHaveBeenCalledWith(
|
||||||
|
't1',
|
||||||
|
expect.objectContaining({
|
||||||
|
email: 'friend@example.com',
|
||||||
|
invitedByUserId: 'owner-1',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const [, fields] = repo.create.mock.calls[0] as [
|
||||||
|
string,
|
||||||
|
{ tokenHash: string },
|
||||||
|
];
|
||||||
|
expect(fields.tokenHash).not.toBe(result.rawToken);
|
||||||
|
});
|
||||||
|
});
|
||||||
69
backend/libs/trips/src/trip-invitations.service.ts
Normal file
69
backend/libs/trips/src/trip-invitations.service.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { randomBytes, createHash } from 'node:crypto';
|
||||||
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
ConflictException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
} from '@nestjs/common';
|
||||||
|
import { TripInvitationsRepository } from './trip-invitations.repository';
|
||||||
|
import { TripMembersRepository } from './trip-members.repository';
|
||||||
|
import type { TripInvitation, TripMember } from './trip.types';
|
||||||
|
|
||||||
|
const DEFAULT_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
|
||||||
|
function hashToken(rawToken: string): string {
|
||||||
|
return createHash('sha256').update(rawToken).digest('hex');
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class TripInvitationsService {
|
||||||
|
constructor(
|
||||||
|
private readonly invitationsRepository: TripInvitationsRepository,
|
||||||
|
private readonly membersRepository: TripMembersRepository,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async createInvitation(
|
||||||
|
tripId: string,
|
||||||
|
invitedByUserId: string,
|
||||||
|
email: string,
|
||||||
|
ttlMs = DEFAULT_TTL_MS,
|
||||||
|
): Promise<{ invitation: TripInvitation; rawToken: string }> {
|
||||||
|
const rawToken = randomBytes(32).toString('base64url');
|
||||||
|
const invitation = await this.invitationsRepository.create(tripId, {
|
||||||
|
email,
|
||||||
|
invitedByUserId,
|
||||||
|
tokenHash: hashToken(rawToken),
|
||||||
|
expiresAt: new Date(Date.now() + ttlMs),
|
||||||
|
});
|
||||||
|
return { invitation, rawToken };
|
||||||
|
}
|
||||||
|
|
||||||
|
listInvitations(tripId: string): Promise<TripInvitation[]> {
|
||||||
|
return this.invitationsRepository.listByTrip(tripId);
|
||||||
|
}
|
||||||
|
|
||||||
|
removeInvitation(tripId: string, invitationId: string): Promise<void> {
|
||||||
|
return this.invitationsRepository.remove(tripId, invitationId);
|
||||||
|
}
|
||||||
|
|
||||||
|
async acceptInvitation(
|
||||||
|
rawToken: string,
|
||||||
|
acceptingUserId: string,
|
||||||
|
): Promise<TripMember> {
|
||||||
|
const invitation = await this.invitationsRepository.findByTokenHash(
|
||||||
|
hashToken(rawToken),
|
||||||
|
);
|
||||||
|
if (!invitation) throw new NotFoundException('Invitation not found');
|
||||||
|
if (invitation.acceptedAt)
|
||||||
|
throw new ConflictException('Invitation has already been accepted');
|
||||||
|
if (invitation.expiresAt.getTime() < Date.now())
|
||||||
|
throw new BadRequestException('Invitation has expired');
|
||||||
|
|
||||||
|
await this.invitationsRepository.markAccepted(invitation.id);
|
||||||
|
return this.membersRepository.upsertActiveMember(
|
||||||
|
invitation.tripId,
|
||||||
|
acceptingUserId,
|
||||||
|
'MEMBER',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -78,6 +78,25 @@ export interface TripMember {
|
|||||||
updatedAt: Date;
|
updatedAt: Date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface TripInvitation {
|
||||||
|
id: string;
|
||||||
|
tripId: string;
|
||||||
|
email: string;
|
||||||
|
invitedByUserId: string;
|
||||||
|
tokenHash: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
acceptedAt: Date | null;
|
||||||
|
createdAt: Date;
|
||||||
|
updatedAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateTripInvitationFields {
|
||||||
|
email: string;
|
||||||
|
invitedByUserId: string;
|
||||||
|
tokenHash: string;
|
||||||
|
expiresAt: Date;
|
||||||
|
}
|
||||||
|
|
||||||
export const DEFAULT_TRIP_SETTINGS: Omit<TripSettings, 'tripId'> = {
|
export const DEFAULT_TRIP_SETTINGS: Omit<TripSettings, 'tripId'> = {
|
||||||
webResearchEnabled: false,
|
webResearchEnabled: false,
|
||||||
periodicAgentReviewEnabled: false,
|
periodicAgentReviewEnabled: false,
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import { TripSettingsService } from './trip-settings.service';
|
|||||||
import { TripMembersRepository } from './trip-members.repository';
|
import { TripMembersRepository } from './trip-members.repository';
|
||||||
import { TripMembersService } from './trip-members.service';
|
import { TripMembersService } from './trip-members.service';
|
||||||
import { TripMembershipGuard } from './trip-membership.guard';
|
import { TripMembershipGuard } from './trip-membership.guard';
|
||||||
|
import { TripInvitationsRepository } from './trip-invitations.repository';
|
||||||
|
import { TripInvitationsService } from './trip-invitations.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [DatabaseModule],
|
imports: [DatabaseModule],
|
||||||
@@ -18,12 +20,15 @@ import { TripMembershipGuard } from './trip-membership.guard';
|
|||||||
TripMembersRepository,
|
TripMembersRepository,
|
||||||
TripMembersService,
|
TripMembersService,
|
||||||
TripMembershipGuard,
|
TripMembershipGuard,
|
||||||
|
TripInvitationsRepository,
|
||||||
|
TripInvitationsService,
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
TripsService,
|
TripsService,
|
||||||
TripSettingsService,
|
TripSettingsService,
|
||||||
TripMembersService,
|
TripMembersService,
|
||||||
TripMembershipGuard,
|
TripMembershipGuard,
|
||||||
|
TripInvitationsService,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class TripsLibModule {}
|
export class TripsLibModule {}
|
||||||
|
|||||||
Reference in New Issue
Block a user