Skip to content

Commit

Permalink
Merge branch 'master' into feat/version
Browse files Browse the repository at this point in the history
  • Loading branch information
Clashsoft authored May 18, 2024
2 parents 3c51716 + e86be93 commit 80b3c19
Show file tree
Hide file tree
Showing 25 changed files with 2,322 additions and 1,842 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/audit.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v3
with:
version: latest
# https://github.com/actions/setup-node/issues/480#issuecomment-1915448139
- name: Enable Corepack
run: corepack enable
- name: Set up Node.js LTS
uses: actions/setup-node@v4
with:
Expand Down
7 changes: 3 additions & 4 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,9 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up pnpm
uses: pnpm/action-setup@v3
with:
version: latest
# https://github.com/actions/setup-node/issues/480#issuecomment-1915448139
- name: Enable Corepack
run: corepack enable
- name: Set up Node.js LTS
uses: actions/setup-node@v4
with:
Expand Down
8 changes: 3 additions & 5 deletions apps/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,16 +1,14 @@
FROM node:lts-slim as builder
RUN npm install -g pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
RUN corepack pnpm install --frozen-lockfile
COPY . .
RUN pnpm run build:backend
RUN corepack pnpm run build:backend

FROM node:lts-slim
RUN npm install -g pnpm
WORKDIR /app
COPY --from=builder /app/dist/apps/backend/ ./
ENV NODE_ENV=production
RUN pnpm install # --frozen-lockfile
RUN corepack pnpm install # --frozen-lockfile
EXPOSE 3000
CMD node main.js
26 changes: 17 additions & 9 deletions apps/backend/src/poll/poll/poll.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,13 +147,21 @@ export class PollController {
return this.pollService.deleteParticipation(id, participantId);
}

@Post(':id/book')
async bookEvents(@Param('id', ObjectIdPipe) id: Types.ObjectId, @Body() events: string[]): Promise<ReadPollDto> {
const poll = await this.pollService.getPoll(id);
if (!poll) {
throw new NotFoundException(id);
}

return this.pollService.bookEvents(id, events.map(e => new Types.ObjectId(e)));
}
@Post(':id/book')
async bookEvents(
@Param('id', ObjectIdPipe) id: Types.ObjectId,
@Body() events: Record<string, string[] | true>,
): Promise<ReadPollDto> {
const poll = await this.pollService.getPoll(id);
if (!poll) {
throw new NotFoundException(id);
}

// convert nested strings to ObjectIds
const bookedEvents = Object.fromEntries(Object
.entries(events)
.map(([key, value]) => [key, value === true ? true as const : value.map(v => new Types.ObjectId(v))]),
);
return this.pollService.bookEvents(id, bookedEvents);
}
}
75 changes: 61 additions & 14 deletions apps/backend/src/poll/poll/poll.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export class PollService implements OnModuleInit {
this.migrateSelection(),
this.migratePollEvents(),
this.migrateShowResults(),
this.migrateBookedEvents(),
]);
}

Expand Down Expand Up @@ -114,6 +115,26 @@ export class PollService implements OnModuleInit {
result.modifiedCount && this.logger.log(`Migrated ${result.modifiedCount} polls to the new show result format.`);
}

private async migrateBookedEvents() {
// migrate all Poll's bookedEvents: ObjectId[] to Record<ObjectId, true>
const polls = await this.pollModel.find({bookedEvents: {$type: 'array'}}).exec();
if (!polls.length) {
return;
}

for (const poll of polls) {
const bookedEvents = poll.bookedEvents as unknown as Types.ObjectId[];
const newBookedEvents: Record<string, true> = {};
for (const event of bookedEvents) {
newBookedEvents[event.toString()] = true;
}
poll.bookedEvents = newBookedEvents;
poll.markModified('bookedEvents');
}
await this.pollModel.bulkSave(polls, {timestamps: false});
this.logger.log(`Migrated ${polls.length} polls to the new booked events format.`);
}

private activeFilter(active: boolean | undefined): FilterQuery<Poll> {
if (active === undefined) {
return {};
Expand Down Expand Up @@ -317,7 +338,7 @@ export class PollService implements OnModuleInit {
}

private async sendAdminInfo(poll: Poll & Document, participant: Participant & Document) {
const events = await this.getEvents(poll._id.toString());
const events = await this.getEvents(poll._id);
const participation = Array(events.length).fill({});

for (let i = 0; i < events.length; i++) {
Expand Down Expand Up @@ -360,29 +381,55 @@ export class PollService implements OnModuleInit {
return this.participantModel.findByIdAndDelete(participantId, {projection: readParticipantSelect}).exec();
}

async bookEvents(id: Types.ObjectId, events: Types.ObjectId[]): Promise<ReadPollDto> {
async bookEvents(id: Types.ObjectId, events: Poll['bookedEvents']): Promise<ReadPollDto> {
const poll = await this.pollModel.findByIdAndUpdate(id, {
bookedEvents: events,
}, {new: true})
.populate<{bookedEvents: PollEvent[]}>('bookedEvents')
.select(readPollSelect)
.exec();
for await (const participant of this.participantModel.find({poll: new Types.ObjectId(id)})) {
const appointments = poll.bookedEvents.map(event => {
let eventLine = this.renderEvent(event, undefined, poll.timeZone);
const selection = participant.selection[event._id.toString()];
if (selection === 'yes' || selection === 'maybe') {
eventLine += ' *';
}
return eventLine;
});
const eventDocs = await this.pollEventModel.find({
poll: id,
_id: {$in: Object.keys(events).map(e => new Types.ObjectId(e))},
});
for await (const participant of this.participantModel.find({
poll: new Types.ObjectId(id),
mail: {$exists: true},
})) {
const appointments = eventDocs
.filter(event => {
const booked = events[event._id.toString()];
// only show the events to the participant that are either
if (booked === true) {
// 1) booked entirely, or
return true;
} else if (Array.isArray(booked)) {
// 2) booked for the participant
return booked.some(id => participant._id.equals(id));
} else {
return false;
}
})
.map(event => {
let eventLine = this.renderEvent(event, undefined, poll.timeZone);
const selection = participant.selection[event._id.toString()];
if (selection === 'yes' || selection === 'maybe') {
eventLine += ' *';
}
return eventLine;
});

if (!appointments.length) {
// don't send them an email if there are no appointments
continue;
}

this.mailService.sendMail(participant.name, participant.mail, 'Poll booked', 'book', {
appointments,
poll: poll.toObject(),
participant: participant.toObject(),
}).then();
}).catch(console.error);
}
return {...poll.toObject(), bookedEvents: events};
return poll;
}

private renderEvent(event: PollEvent, locale?: string, timeZone?: string) {
Expand Down
19 changes: 19 additions & 0 deletions apps/backend/src/push/push.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {PushConfigDto} from '@apollusia/types';
import {NotFound} from '@mean-stream/nestx/not-found';
import {Controller, Get} from '@nestjs/common';

import {PushService} from './push.service';

@Controller('push')
export class PushController {
constructor(
private pushService: PushService,
) {
}

@Get('config')
@NotFound()
getConfig(): PushConfigDto | undefined {
return this.pushService.config;
}
}
12 changes: 7 additions & 5 deletions apps/backend/src/push/push.module.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
import {Module} from '@nestjs/common';
import {ConfigModule} from '@nestjs/config';

import {PushController} from './push.controller';
import {PushService} from './push.service';

@Module({
imports: [
ConfigModule,
],
providers: [PushService],
exports: [PushService],
imports: [
ConfigModule,
],
providers: [PushService],
controllers: [PushController],
exports: [PushService],
})
export class PushModule {
}
59 changes: 33 additions & 26 deletions apps/backend/src/push/push.service.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import {PushConfigDto} from '@apollusia/types';
import {Injectable, Logger} from '@nestjs/common';
import {ConfigService} from '@nestjs/config';
import * as webpush from 'web-push';
Expand All @@ -7,32 +8,38 @@ import {PushSubscription} from 'web-push';
export class PushService {
private logger = new Logger(PushService.name);

constructor(
private config: ConfigService,
) {
const publicKey = config.get('VAPID_PUBLIC_KEY');
const privateKey = config.get('VAPID_PRIVATE_KEY');
const emailSender = config.get('EMAIL_FROM');
if (publicKey && privateKey && emailSender) {
webpush.setVapidDetails('mailto:' + emailSender, publicKey, privateKey);
} else {
this.logger.warn('VAPID keys not set. Push notifications will not work.');
}
}
config?: PushConfigDto;

constructor(
config: ConfigService,
) {
const publicKey = config.get('VAPID_PUBLIC_KEY');
const privateKey = config.get('VAPID_PRIVATE_KEY');
const emailSender = config.get('EMAIL_FROM');
if (publicKey && privateKey && emailSender) {
webpush.setVapidDetails('mailto:' + emailSender, publicKey, privateKey);

async send(sub: PushSubscription, title: string, body: string, url: string) {
const payload = {
notification: {
title,
body,
// icon: 'assets/main-page-logo-small-hat.png',
data: {
onActionClick: {
'default': {operation: 'openWindow', url},
},
},
},
};
await webpush.sendNotification(sub, JSON.stringify(payload));
this.config = {
vapidPublicKey: publicKey,
};
} else {
this.logger.warn('VAPID keys not set. Push notifications will not work.');
}
}

async send(sub: PushSubscription, title: string, body: string, url: string) {
const payload = {
notification: {
title,
body,
// icon: 'assets/main-page-logo-small-hat.png',
data: {
onActionClick: {
'default': {operation: 'openWindow', url},
},
},
},
};
await webpush.sendNotification(sub, JSON.stringify(payload));
}
}
2 changes: 1 addition & 1 deletion apps/backend/test/stubs/PollStub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,6 @@ export const PollStub = (): PollDto => {
showResult: ShowResultOptions.IMMEDIATELY,
},
adminToken: '619b3a00-2dc3-48f1-8b3d-50386a91a559',
bookedEvents: [],
bookedEvents: {},
};
};
5 changes: 2 additions & 3 deletions apps/frontend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
FROM node:lts-slim as builder
WORKDIR /app
RUN npm install -g pnpm
COPY package.json pnpm-lock.yaml ./
RUN pnpm install
RUN corepack pnpm install --frozen-lockfile
COPY . .
ARG APP_VERSION
RUN APP_VERSION=$APP_VERSION pnpm run build:frontend
RUN APP_VERSION=$APP_VERSION corepack pnpm run build:frontend

FROM node:lts-slim
WORKDIR /app
Expand Down
4 changes: 2 additions & 2 deletions apps/frontend/src/app/pipes/some.pipe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {Pipe, PipeTransform} from '@angular/core';
name: 'some',
})
export class SomePipe implements PipeTransform {
transform<T>(array: T[], search: T): boolean {
return array.includes(search);
transform<T>(array: T[] | unknown, search: T): boolean {
return Array.isArray(array) && array.includes(search);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import {HttpClient} from '@angular/common/http';
import {Component, OnInit} from '@angular/core';
import {FormControl, FormGroup, Validators} from '@angular/forms';
import {ActivatedRoute, Router} from '@angular/router';
import {SwPush} from '@angular/service-worker';
import {ShowResultOptions} from '@apollusia/types/lib/schema/show-result-options';
import {NgbModal} from '@ng-bootstrap/ng-bootstrap';
import {format} from 'date-fns';
Expand All @@ -12,6 +11,7 @@ import {map} from 'rxjs/operators';
import {environment} from '../../../environments/environment';
import {MailService, TokenService} from '../../core/services';
import {CreatePollDto, Poll} from '../../model';
import {PushService} from '../services/push.service';

@Component({
selector: 'app-create-edit-poll',
Expand Down Expand Up @@ -90,10 +90,10 @@ export class CreateEditPollComponent implements OnInit {
private modalService: NgbModal,
private http: HttpClient,
private router: Router,
private route: ActivatedRoute,
private pushService: PushService,
private tokenService: TokenService,
private mailService: MailService,
private swPush: SwPush,
route: ActivatedRoute,
) {
const routeId: Observable<string> = route.params.pipe(map(({id}) => id));
routeId.subscribe((id: string) => {
Expand Down Expand Up @@ -121,9 +121,7 @@ export class CreateEditPollComponent implements OnInit {
async onFormSubmit() {
const pollForm = this.pollForm.value;
const deadline = pollForm.deadlineDate ? new Date(pollForm.deadlineDate + ' ' + (pollForm.deadlineTime || '00:00')) : undefined;
const pushToken = pollForm.pushUpdates ? await this.swPush.requestSubscription({
serverPublicKey: environment.vapidPublicKey,
}) : undefined;
const pushToken = pollForm.pushUpdates ? await this.pushService.getPushToken().catch(() => undefined) : undefined;
const createPollDto: CreatePollDto & {adminToken: string} = {
title: pollForm.title!,
description: pollForm.description ? pollForm.description : '',
Expand All @@ -132,7 +130,7 @@ export class CreateEditPollComponent implements OnInit {
adminMail: pollForm.emailUpdates ? this.poll?.adminMail || this.mail : undefined,
adminPush: pollForm.pushUpdates && (this.poll?.adminPush || pushToken?.toJSON()) || undefined,
timeZone: Intl.DateTimeFormat().resolvedOptions().timeZone,
bookedEvents: [],
bookedEvents: {},
settings: {
deadline: deadline?.toISOString(),
allowMaybe: !!pollForm.allowMaybe,
Expand Down
Loading

0 comments on commit 80b3c19

Please sign in to comment.