From f9840505b82fcb04364a79576f186b76cc75f5c0 Mon Sep 17 00:00:00 2001
From: Elias Schneider
Date: Tue, 21 Feb 2023 08:51:04 +0100
Subject: [PATCH] feat: invite new user with email
---
backend/prisma/seed/config.seed.ts | 30 ++++++++++---
backend/src/email/email.service.ts | 14 ++++++
backend/src/user/dto/createUser.dto.ts | 9 ++--
backend/src/user/user.module.ts | 2 +
backend/src/user/user.service.ts | 18 +++++++-
.../admin/{ => users}/ManageUserTable.tsx | 2 +-
.../admin/{ => users}/showCreateUserModal.tsx | 44 +++++++++++++++----
.../admin/{ => users}/showUpdateUserModal.tsx | 8 ++--
frontend/src/pages/admin/users.tsx | 11 +++--
frontend/src/types/user.type.ts | 2 +-
10 files changed, 111 insertions(+), 29 deletions(-)
rename frontend/src/components/admin/{ => users}/ManageUserTable.tsx (98%)
rename frontend/src/components/admin/{ => users}/showCreateUserModal.tsx (57%)
rename frontend/src/components/admin/{ => users}/showUpdateUserModal.tsx (93%)
diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts
index 352ab5e..3fecd57 100644
--- a/backend/prisma/seed/config.seed.ts
+++ b/backend/prisma/seed/config.seed.ts
@@ -135,9 +135,27 @@ const configVariables: Prisma.ConfigCreateInput[] = [
"Hey!\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\nPingvin Share 🐧",
category: "email",
},
-
{
order: 13,
+ key: "INVITE_EMAIL_SUBJECT",
+ description:
+ "Subject of the email which gets sent when an admin invites an user.",
+ type: "string",
+ value: "Pingvin Share invite",
+ category: "email",
+ },
+ {
+ order: 14,
+ key: "INVITE_EMAIL_MESSAGE",
+ description:
+ "Message which gets sent when an admin invites an user. {url} will be replaced with the invite URL and {password} with the password.",
+ type: "text",
+ value:
+ "Hey!\nYou were invited to Pingvin Share. Click this link to accept the invite: {url}\nYour password is: {password}\nPingvin Share 🐧",
+ category: "email",
+ },
+ {
+ order: 15,
key: "SMTP_ENABLED",
description:
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
@@ -147,7 +165,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
secret: false,
},
{
- order: 14,
+ order: 16,
key: "SMTP_HOST",
description: "Host of the SMTP server",
type: "string",
@@ -155,7 +173,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
category: "smtp",
},
{
- order: 15,
+ order: 17,
key: "SMTP_PORT",
description: "Port of the SMTP server",
type: "number",
@@ -163,7 +181,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
category: "smtp",
},
{
- order: 16,
+ order: 18,
key: "SMTP_EMAIL",
description: "Email address which the emails get sent from",
type: "string",
@@ -171,7 +189,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
category: "smtp",
},
{
- order: 17,
+ order: 19,
key: "SMTP_USERNAME",
description: "Username of the SMTP server",
type: "string",
@@ -179,7 +197,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
category: "smtp",
},
{
- order: 18,
+ order: 20,
key: "SMTP_PASSWORD",
description: "Password of the SMTP server",
type: "string",
diff --git a/backend/src/email/email.service.ts b/backend/src/email/email.service.ts
index 8e9bf46..ff89a44 100644
--- a/backend/src/email/email.service.ts
+++ b/backend/src/email/email.service.ts
@@ -73,6 +73,20 @@ export class EmailService {
});
}
+ async sendInviteEmail(recipientEmail: string, password: string) {
+ const loginUrl = `${this.config.get("APP_URL")}/auth/signIn`;
+
+ await this.getTransporter().sendMail({
+ from: `"Pingvin Share" <${this.config.get("SMTP_EMAIL")}>`,
+ to: recipientEmail,
+ subject: this.config.get("INVITE_EMAIL_SUBJECT"),
+ text: this.config
+ .get("INVITE_EMAIL_MESSAGE")
+ .replaceAll("{url}", loginUrl)
+ .replaceAll("{password}", password),
+ });
+ }
+
async sendTestMail(recipientEmail: string) {
try {
await this.getTransporter().sendMail({
diff --git a/backend/src/user/dto/createUser.dto.ts b/backend/src/user/dto/createUser.dto.ts
index 986502c..9a1931f 100644
--- a/backend/src/user/dto/createUser.dto.ts
+++ b/backend/src/user/dto/createUser.dto.ts
@@ -1,12 +1,15 @@
-import { Expose, plainToClass } from "class-transformer";
-import { Allow } from "class-validator";
+import { plainToClass } from "class-transformer";
+import { Allow, IsOptional, MinLength } from "class-validator";
import { UserDTO } from "./user.dto";
export class CreateUserDTO extends UserDTO {
- @Expose()
@Allow()
isAdmin: boolean;
+ @MinLength(8)
+ @IsOptional()
+ password: string;
+
from(partial: Partial) {
return plainToClass(CreateUserDTO, partial, {
excludeExtraneousValues: true,
diff --git a/backend/src/user/user.module.ts b/backend/src/user/user.module.ts
index 97150ce..4ca2e94 100644
--- a/backend/src/user/user.module.ts
+++ b/backend/src/user/user.module.ts
@@ -1,8 +1,10 @@
import { Module } from "@nestjs/common";
+import { EmailModule } from "src/email/email.module";
import { UserController } from "./user.controller";
import { UserSevice } from "./user.service";
@Module({
+ imports:[EmailModule],
providers: [UserSevice],
controllers: [UserController],
})
diff --git a/backend/src/user/user.service.ts b/backend/src/user/user.service.ts
index 52d514d..fe3fc30 100644
--- a/backend/src/user/user.service.ts
+++ b/backend/src/user/user.service.ts
@@ -1,13 +1,17 @@
import { BadRequestException, Injectable } from "@nestjs/common";
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
import * as argon from "argon2";
+import { EmailService } from "src/email/email.service";
import { PrismaService } from "src/prisma/prisma.service";
import { CreateUserDTO } from "./dto/createUser.dto";
import { UpdateUserDto } from "./dto/updateUser.dto";
@Injectable()
export class UserSevice {
- constructor(private prisma: PrismaService) {}
+ constructor(
+ private prisma: PrismaService,
+ private emailService: EmailService
+ ) {}
async list() {
return await this.prisma.user.findMany();
@@ -18,7 +22,17 @@ export class UserSevice {
}
async create(dto: CreateUserDTO) {
- const hash = await argon.hash(dto.password);
+ let hash: string;
+
+ // The password can be undefined if the user is invited by an admin
+ if (!dto.password) {
+ const randomPassword = crypto.randomUUID();
+ hash = await argon.hash(randomPassword);
+ this.emailService.sendInviteEmail(dto.email, randomPassword);
+ } else {
+ hash = await argon.hash(dto.password);
+ }
+
try {
return await this.prisma.user.create({
data: {
diff --git a/frontend/src/components/admin/ManageUserTable.tsx b/frontend/src/components/admin/users/ManageUserTable.tsx
similarity index 98%
rename from frontend/src/components/admin/ManageUserTable.tsx
rename to frontend/src/components/admin/users/ManageUserTable.tsx
index 11fbbe4..5ca2272 100644
--- a/frontend/src/components/admin/ManageUserTable.tsx
+++ b/frontend/src/components/admin/users/ManageUserTable.tsx
@@ -1,7 +1,7 @@
import { ActionIcon, Box, Group, Skeleton, Table } from "@mantine/core";
import { useModals } from "@mantine/modals";
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
-import User from "../../types/user.type";
+import User from "../../../types/user.type";
import showUpdateUserModal from "./showUpdateUserModal";
const ManageUserTable = ({
diff --git a/frontend/src/components/admin/showCreateUserModal.tsx b/frontend/src/components/admin/users/showCreateUserModal.tsx
similarity index 57%
rename from frontend/src/components/admin/showCreateUserModal.tsx
rename to frontend/src/components/admin/users/showCreateUserModal.tsx
index a794090..bacd673 100644
--- a/frontend/src/components/admin/showCreateUserModal.tsx
+++ b/frontend/src/components/admin/users/showCreateUserModal.tsx
@@ -10,38 +10,44 @@ import {
import { useForm, yupResolver } from "@mantine/form";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import * as yup from "yup";
-import userService from "../../services/user.service";
-import toast from "../../utils/toast.util";
+import userService from "../../../services/user.service";
+import toast from "../../../utils/toast.util";
const showCreateUserModal = (
modals: ModalsContextProps,
+ smtpEnabled: boolean,
getUsers: () => void
) => {
return modals.openModal({
title: Create user,
- children: ,
+ children: (
+
+ ),
});
};
const Body = ({
modals,
+ smtpEnabled,
getUsers,
}: {
modals: ModalsContextProps;
+ smtpEnabled: boolean;
getUsers: () => void;
}) => {
const form = useForm({
initialValues: {
username: "",
email: "",
- password: "",
+ password: undefined,
isAdmin: false,
+ setPasswordManually: false,
},
validate: yupResolver(
yup.object().shape({
email: yup.string().email(),
username: yup.string().min(3),
- password: yup.string().min(8),
+ password: yup.string().min(8).optional(),
})
),
});
@@ -62,14 +68,34 @@ const Body = ({
-
+ {smtpEnabled && (
+
+ )}
+ {form.values.setPasswordManually || !smtpEnabled && (
+
+ )}
diff --git a/frontend/src/components/admin/showUpdateUserModal.tsx b/frontend/src/components/admin/users/showUpdateUserModal.tsx
similarity index 93%
rename from frontend/src/components/admin/showUpdateUserModal.tsx
rename to frontend/src/components/admin/users/showUpdateUserModal.tsx
index d87c84d..c15338e 100644
--- a/frontend/src/components/admin/showUpdateUserModal.tsx
+++ b/frontend/src/components/admin/users/showUpdateUserModal.tsx
@@ -11,9 +11,9 @@ import {
import { useForm, yupResolver } from "@mantine/form";
import { ModalsContextProps } from "@mantine/modals/lib/context";
import * as yup from "yup";
-import userService from "../../services/user.service";
-import User from "../../types/user.type";
-import toast from "../../utils/toast.util";
+import userService from "../../../services/user.service";
+import User from "../../../types/user.type";
+import toast from "../../../utils/toast.util";
const showUpdateUserModal = (
modals: ModalsContextProps,
@@ -90,7 +90,7 @@ const Body = ({
- Change password
+ Change password