Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion backend/@types/express/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ import type {
Bed,
Room,
File,
Newsletter,
NewsletterManager,
NewsletterSubscriber,
} from '@prisma/client';
import type { ZodObject, z } from 'zod';
import type { JsonResource } from '#core/resource/JsonResource';
Expand All @@ -23,10 +26,13 @@ declare global {
tableTemplate?: TableTemplate;
message?: Message & { attachments: File[] };
messageTemplate?: MessageTemplate & { attachments: File[] };
manager?: CampManager;
campManager?: CampManager;
room?: Room & { beds: Bed[] };
bed?: Bed;
file?: File;
newsletter?: Newsletter;
newsletterManager?: NewsletterManager;
subscriber?: NewsletterSubscriber;
}

interface AuthUser {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
-- CreateTable
CREATE TABLE `newsletters` (
`id` CHAR(26) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`description` TEXT NULL,
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`updated_at` DATETIME(3) NULL,

UNIQUE INDEX `newsletters_id_unique`(`id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `newsletter_managers` (
`id` CHAR(26) NOT NULL,
`newsletter_id` CHAR(26) NOT NULL,
`user_id` CHAR(26) NOT NULL,

UNIQUE INDEX `newsletter_managers_id_unique`(`id`),
UNIQUE INDEX `newsletter_managers_newsletter_id_user_id_unique`(`newsletter_id`, `user_id`),
INDEX `newsletter_managers_newsletter_id_index`(`newsletter_id`),
INDEX `newsletter_managers_user_id_index`(`user_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- CreateTable
CREATE TABLE `newsletter_subscribers` (
`id` CHAR(26) NOT NULL,
`newsletter_id` CHAR(26) NOT NULL,
`email` VARCHAR(255) NOT NULL,
`name` VARCHAR(255) NULL,
`country` VARCHAR(5) NULL,
`unsubscribe_token` CHAR(64) NOT NULL,
`subscribed_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),

UNIQUE INDEX `newsletter_subscribers_id_unique`(`id`),
UNIQUE INDEX `newsletter_subscribers_unsubscribe_token_unique`(`unsubscribe_token`),
UNIQUE INDEX `newsletter_subscribers_newsletter_id_email_unique`(`newsletter_id`, `email`),
INDEX `newsletter_subscribers_newsletter_id_index`(`newsletter_id`),
PRIMARY KEY (`id`)
) DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

-- AddForeignKey
ALTER TABLE `newsletter_managers` ADD CONSTRAINT `newsletter_managers_newsletter_id_foreign` FOREIGN KEY (`newsletter_id`) REFERENCES `newsletters`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `newsletter_managers` ADD CONSTRAINT `newsletter_managers_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE `newsletter_subscribers` ADD CONSTRAINT `newsletter_subscribers_newsletter_id_foreign` FOREIGN KEY (`newsletter_id`) REFERENCES `newsletters`(`id`) ON DELETE CASCADE ON UPDATE CASCADE;
44 changes: 44 additions & 0 deletions backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ model User {

campRoles CampManager[]
tokens Token[]
newsletterManagers NewsletterManager[]

@@map("users")
}
Expand Down Expand Up @@ -303,3 +304,46 @@ model JobRateLimit {

@@map("job_rate_limits")
}

model Newsletter {
id String @id @unique(map: "newsletters_id_unique") @default(ulid()) @db.Char(26)
name String @db.VarChar(255)
description String? @db.Text
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime? @updatedAt @map("updated_at")

managers NewsletterManager[]
subscribers NewsletterSubscriber[]

@@map("newsletters")
}

model NewsletterManager {
id String @id @unique(map: "newsletter_managers_id_unique") @default(ulid()) @db.Char(26)
newsletterId String @map("newsletter_id") @db.Char(26)
userId String @map("user_id") @db.Char(26)

newsletter Newsletter @relation(fields: [newsletterId], references: [id], onDelete: Cascade, onUpdate: Cascade, map: "newsletter_managers_newsletter_id_foreign")
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade, map: "newsletter_managers_user_id_foreign")

@@unique([newsletterId, userId], map: "newsletter_managers_newsletter_id_user_id_unique")
@@index([newsletterId], map: "newsletter_managers_newsletter_id_index")
@@index([userId], map: "newsletter_managers_user_id_index")
@@map("newsletter_managers")
}

model NewsletterSubscriber {
id String @id @unique(map: "newsletter_subscribers_id_unique") @default(ulid()) @db.Char(26)
newsletterId String @map("newsletter_id") @db.Char(26)
email String @db.VarChar(255)
name String? @db.VarChar(255)
country String? @db.VarChar(5)
unsubscribeToken String @unique(map: "newsletter_subscribers_unsubscribe_token_unique") @map("unsubscribe_token") @db.Char(64)
subscribedAt DateTime @default(now()) @map("subscribed_at")

newsletter Newsletter @relation(fields: [newsletterId], references: [id], onDelete: Cascade, onUpdate: Cascade, map: "newsletter_subscribers_newsletter_id_foreign")

@@unique([newsletterId, email], map: "newsletter_subscribers_newsletter_id_email_unique")
@@index([newsletterId], map: "newsletter_subscribers_newsletter_id_index")
@@map("newsletter_subscribers")
}
2 changes: 1 addition & 1 deletion backend/prisma/seeders/camp-manager.seeder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { CampManagerFactory } from '../factories';

class CampManagerSeeder extends BaseSeeder {
name(): string {
return 'manager';
return 'camp-manager';
}

async run(): Promise<void> {
Expand Down
5 changes: 3 additions & 2 deletions backend/src/app/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { type Request, type Response } from 'express';
import type { AuthTokensResponse } from '#types/response';
import type { AppConfig } from '#config/index';
import ApiError from '#utils/ApiError';
import { ManagerService } from '#app/manager/manager.service';
import { CampManagerService } from '#app/campManager/camp-manager.service.js';
import authResource from './auth.resource.js';
import validator from './auth.validation.js';
import { TotpService } from '#app/totp/totp.service';
Expand All @@ -24,7 +24,8 @@ export class AuthController extends BaseController {
@Config() private readonly config: AppConfig,
@inject(AuthService) private readonly authService: AuthService,
@inject(UserService) private readonly userService: UserService,
@inject(ManagerService) private readonly managerService: ManagerService,
@inject(CampManagerService)
private readonly managerService: CampManagerService,
@inject(TokenService) private readonly tokenService: TokenService,
@inject(TotpService) private readonly totpService: TotpService,
) {
Expand Down
5 changes: 3 additions & 2 deletions backend/src/app/camp/camp.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import validator from './camp.validation.js';
import type { Request, Response } from 'express';
import { BaseController } from '#core/base/BaseController';
import { MessageTemplateService } from '#app/messageTemplate/message-template.service';
import { ManagerService } from '#app/manager/manager.service';
import { CampManagerService } from '#app/campManager/camp-manager.service.js';
import ApiError from '#utils/ApiError';
import { inject, injectable } from 'inversify';

Expand All @@ -22,7 +22,8 @@ export class CampController extends BaseController {
constructor(
@inject(CampService) private readonly campService: CampService,
@inject(FileService) private readonly fileService: FileService,
@inject(ManagerService) private readonly managerService: ManagerService,
@inject(CampManagerService)
private readonly managerService: CampManagerService,
@inject(RegistrationService)
private readonly registrationService: RegistrationService,
@inject(TableTemplateService)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,19 @@
import ApiError from '#utils/ApiError';
import httpStatus from 'http-status';
import { UserService } from '#app/user/user.service';
import { ManagerService } from '#app/manager/manager.service';
import { ManagerResource } from '#app/manager/manager.resource';
import validator from '#app/manager/manager.validation';
import { CampManagerService } from '#app/campManager/camp-manager.service.js';
import { CampManagerResource } from '#app/campManager/camp-manager.resource.js';
import validator from '#app/campManager/camp-manager.validation';
import { type Request, type Response } from 'express';
import { ManagerInvitationMessage } from '#app/manager/manager.messages';
import { CampManagerInvitationMessage } from '#app/campManager/camp-manager.messages';
import { BaseController } from '#core/base/BaseController';
import { inject, injectable } from 'inversify';

@injectable()
export class ManagerController extends BaseController {
export class CampManagerController extends BaseController {
constructor(
@inject(ManagerService) private readonly managerService: ManagerService,
@inject(CampManagerService)
private readonly managerService: CampManagerService,
@inject(UserService) private readonly userService: UserService,
) {
super();
Expand All @@ -25,7 +26,7 @@ export class ManagerController extends BaseController {

const managers = await this.managerService.getManagers(campId);

res.resource(ManagerResource.collection(managers));
res.resource(CampManagerResource.collection(managers));
}

async store(req: Request, res: Response) {
Expand Down Expand Up @@ -57,16 +58,16 @@ export class ManagerController extends BaseController {
? await this.managerService.inviteManager(camp.id, email, data)
: await this.managerService.addManager(camp.id, user.id, data);

await ManagerInvitationMessage.enqueue({
await CampManagerInvitationMessage.enqueue({
camp,
manager,
});

res.status(httpStatus.CREATED).resource(new ManagerResource(manager));
res.status(httpStatus.CREATED).resource(new CampManagerResource(manager));
}

async update(req: Request, res: Response) {
const manager = req.modelOrFail('manager');
const manager = req.modelOrFail('message');
const {
body: { role, expiresAt },
} = await req.validate(validator.update);
Expand All @@ -79,7 +80,7 @@ export class ManagerController extends BaseController {
},
);

res.resource(new ManagerResource(updatedManager));
res.resource(new CampManagerResource(updatedManager));
}

async destroy(req: Request, res: Response) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ type CampManagerWithUserOrInvitation = CampManager & { user: User | null } & {
invitation: Invitation | null;
};

abstract class ManagerMessage<
abstract class CampManagerMessage<
T extends { manager: CampManagerWithUserOrInvitation },
> extends MailBase<T> {
protected to() {
Expand Down Expand Up @@ -36,7 +36,7 @@ abstract class ManagerMessage<
}
}

export class ManagerInvitationMessage extends ManagerMessage<{
export class CampManagerInvitationMessage extends CampManagerMessage<{
manager: CampManagerWithUserOrInvitation;
camp: Camp;
}> {
Expand Down Expand Up @@ -71,7 +71,7 @@ export class ManagerInvitationMessage extends ManagerMessage<{
protected content() {
const camp = this.payload.camp;
const campName = translateObject(camp.name, this.locale());
const url = generateUrl(['management', camp.id]);
const url = generateUrl(['management', 'camps', camp.id]);

return {
template: 'manager-invitation',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@ import type {
BindOptions,
ModuleOptions,
} from '#core/base/AppModule';
import { ManagerRouter } from '#app/manager/manager.routes';
import { CampManagerRouter } from '#app/campManager/camp-manager.routes';
import type { ManagerPermission } from '@camp-registration/common/permissions';
import { ManagerController } from '#app/manager/manager.controller';
import { ManagerService } from '#app/manager/manager.service';
import { CampManagerController } from '#app/campManager/camp-manager.controller';
import { CampManagerService } from '#app/campManager/camp-manager.service';
import { MailableRegistry } from '#app/mail/mail.registry';
import { ManagerInvitationMessage } from '#app/manager/manager.messages';
import { CampManagerInvitationMessage } from '#app/campManager/camp-manager.messages';
import { resolve } from '#core/ioc/container';

export class ManagerModule implements AppModule {
export class CampManagerModule implements AppModule {
bindContainers(options: BindOptions) {
options.bind(ManagerController).toSelf().inSingletonScope();
options.bind(ManagerService).toSelf().inSingletonScope();
options.bind(CampManagerController).toSelf().inSingletonScope();
options.bind(CampManagerService).toSelf().inSingletonScope();
}

configure(_options: ModuleOptions): Promise<void> | void {
resolve(MailableRegistry).register(ManagerInvitationMessage);
resolve(MailableRegistry).register(CampManagerInvitationMessage);
}

registerRoutes(router: AppRouter): void {
router.useRouter('/camps/:campId/managers', new ManagerRouter());
router.useRouter('/camps/:campId/managers', new CampManagerRouter());
}

registerPermissions(): RoleToPermissions<ManagerPermission> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export interface ManagerWithRelationships extends CampManager {
invitation: Invitation | null;
}

export class ManagerResource extends JsonResource<
export class CampManagerResource extends JsonResource<
ManagerWithRelationships,
CampManagerData
> {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,22 @@
import { auth, guard } from '#middlewares/index';
import { campManager } from '#guards/manager.guard';
import { ManagerController } from './manager.controller.js';
import { CampManagerController } from './camp-manager.controller.js';
import { controller } from '#utils/bindController';
import { ModuleRouter } from '#core/router/ModuleRouter';
import { ManagerService } from '#app/manager/manager.service';
import { CampManagerService } from '#app/campManager/camp-manager.service.js';
import { resolve } from '#core/ioc/container';

export class ManagerRouter extends ModuleRouter {
export class CampManagerRouter extends ModuleRouter {
protected registerBindings() {
const managerService = resolve(ManagerService);
this.bindModel('manager', (req, id) => {
const managerService = resolve(CampManagerService);
this.bindModel('campManager', (req, id) => {
const camp = req.modelOrFail('camp');
return managerService.getManagerById(camp.id, id);
});
}

protected defineRoutes() {
const managerController = resolve(ManagerController);
const managerController = resolve(CampManagerController);

this.router.use(auth());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type ManagerUpdateData = Pick<
>;

@injectable()
export class ManagerService extends BaseService {
export class CampManagerService extends BaseService {
async campManagerExistsWithUserIdAndCampId(campId: string, userId: string) {
return this.prisma.campManager
.findFirst({
Expand Down
Loading
Loading