feat: add email recepients functionality

This commit is contained in:
Elias Schneider
2022-11-11 15:12:16 +01:00
parent 0efd2d8bf9
commit 32ad43ae27
15 changed files with 192 additions and 18 deletions

View File

@@ -13,12 +13,14 @@ import { PrismaService } from "./prisma/prisma.service";
import { ShareController } from "./share/share.controller";
import { ShareModule } from "./share/share.module";
import { UserController } from "./user/user.controller";
import { EmailModule } from "./email/email.module";
@Module({
imports: [
AuthModule,
ShareModule,
FileModule,
EmailModule,
PrismaModule,
ConfigModule.forRoot({ isGlobal: true }),
ThrottlerModule.forRoot({

View File

@@ -0,0 +1,8 @@
import { Module } from "@nestjs/common";
import { EmailService } from "./email.service";
@Module({
providers: [EmailService],
exports: [EmailService],
})
export class EmailModule {}

View File

@@ -0,0 +1,35 @@
import { Injectable } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import { User } from "@prisma/client";
import * as nodemailer from "nodemailer";
@Injectable()
export class EmailService {
constructor(private config: ConfigService) {}
// create reusable transporter object using the default SMTP transport
transporter = nodemailer.createTransport({
host: this.config.get("SMTP_HOST"),
port: parseInt(this.config.get("SMTP_PORT")),
secure: parseInt(this.config.get("SMTP_PORT")) == 465,
auth: {
user: this.config.get("SMTP_EMAIL"),
pass: this.config.get("SMTP_PASSWORD"),
},
});
async sendMail(recipientEmail: string, shareId: string, creator: User) {
const shareUrl = `${this.config.get("APP_URL")}/share/${shareId}`;
const creatorIdentifier =
creator.firstName && creator.lastName
? `${creator.firstName} ${creator.lastName}`
: creator.email;
await this.transporter.sendMail({
from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
to: recipientEmail,
subject: "Files shared with you",
text: `Hey!\n${creatorIdentifier} shared some files with you. View or dowload the files with this link: ${shareUrl}.\n Shared securely with Pingvin Share 🐧`,
});
}
}

View File

@@ -1,5 +1,11 @@
import { Type } from "class-transformer";
import { IsString, Length, Matches, ValidateNested } from "class-validator";
import {
IsEmail,
IsString,
Length,
Matches,
ValidateNested,
} from "class-validator";
import { ShareSecurityDTO } from "./shareSecurity.dto";
export class CreateShareDTO {
@@ -13,6 +19,9 @@ export class CreateShareDTO {
@IsString()
expiration: string;
@IsEmail({}, { each: true })
recipients: string[];
@ValidateNested()
@Type(() => ShareSecurityDTO)
security: ShareSecurityDTO;

View File

@@ -8,6 +8,9 @@ export class MyShareDTO extends ShareDTO {
@Expose()
createdAt: Date;
@Expose()
recipients: string[];
from(partial: Partial<MyShareDTO>) {
return plainToClass(MyShareDTO, partial, { excludeExtraneousValues: true });
}

View File

@@ -1,11 +1,12 @@
import { forwardRef, Module } from "@nestjs/common";
import { JwtModule } from "@nestjs/jwt";
import { EmailModule } from "src/email/email.module";
import { FileModule } from "src/file/file.module";
import { ShareController } from "./share.controller";
import { ShareService } from "./share.service";
@Module({
imports: [JwtModule.register({}), forwardRef(() => FileModule)],
imports: [JwtModule.register({}), EmailModule, forwardRef(() => FileModule)],
controllers: [ShareController],
providers: [ShareService],
exports: [ShareService],

View File

@@ -11,6 +11,7 @@ import * as archiver from "archiver";
import * as argon from "argon2";
import * as fs from "fs";
import * as moment from "moment";
import { EmailService } from "src/email/email.service";
import { FileService } from "src/file/file.service";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateShareDTO } from "./dto/createShare.dto";
@@ -20,6 +21,7 @@ export class ShareService {
constructor(
private prisma: PrismaService,
private fileService: FileService,
private emailService: EmailService,
private config: ConfigService,
private jwtService: JwtService
) {}
@@ -36,7 +38,7 @@ export class ShareService {
}
// We have to add an exception for "never" (since moment won't like that)
let expirationDate;
let expirationDate: Date;
if (share.expiration !== "never") {
expirationDate = moment()
.add(
@@ -60,6 +62,11 @@ export class ShareService {
expiration: expirationDate,
creator: { connect: user ? { id: user.id } : undefined },
security: { create: share.security },
recipients: {
create: share.recipients
? share.recipients.map((email) => ({ email }))
: [],
},
},
});
}
@@ -84,21 +91,33 @@ export class ShareService {
}
async complete(id: string) {
const share = await this.prisma.share.findUnique({
where: { id },
include: { files: true, recipients: true, creator: true },
});
if (await this.isShareCompleted(id))
throw new BadRequestException("Share already completed");
const moreThanOneFileInShare =
(await this.prisma.file.findMany({ where: { shareId: id } })).length != 0;
if (!moreThanOneFileInShare)
if (share.files.length == 0)
throw new BadRequestException(
"You need at least on file in your share to complete it."
);
// Asynchronously create a zip of all files
this.createZip(id).then(() =>
this.prisma.share.update({ where: { id }, data: { isZipReady: true } })
);
// Send email for each recepient
for (const recepient of share.recipients) {
await this.emailService.sendMail(
recepient.email,
share.id,
share.creator
);
}
return await this.prisma.share.update({
where: { id },
data: { uploadLocked: true },
@@ -106,7 +125,7 @@ export class ShareService {
}
async getSharesByUser(userId: string) {
return await this.prisma.share.findMany({
const shares = await this.prisma.share.findMany({
where: {
creator: { id: userId },
uploadLocked: true,
@@ -119,7 +138,17 @@ export class ShareService {
orderBy: {
expiration: "desc",
},
include: { recipients: true },
});
const sharesWithEmailRecipients = shares.map((share) => {
return {
...share,
recipients: share.recipients.map((recipients) => recipients.email),
};
});
return sharesWithEmailRecipients;
}
async get(id: string) {