diff --git a/backend/prisma/seed/config.seed.ts b/backend/prisma/seed/config.seed.ts index 352ab5e9..3fecd571 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 8e9bf463..ff89a442 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 986502cc..9a1931fd 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 97150ced..4ca2e946 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 52d514dd..fe3fc305 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 11fbbe4b..5ca22725 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 a794090e..bacd673b 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 d87c84dd..c15338e1 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
{ diff --git a/frontend/src/pages/admin/users.tsx b/frontend/src/pages/admin/users.tsx index eb307beb..017269ea 100644 --- a/frontend/src/pages/admin/users.tsx +++ b/frontend/src/pages/admin/users.tsx @@ -2,9 +2,10 @@ import { Button, Group, Space, Text, Title } from "@mantine/core"; import { useModals } from "@mantine/modals"; import { useEffect, useState } from "react"; import { TbPlus } from "react-icons/tb"; -import ManageUserTable from "../../components/admin/ManageUserTable"; -import showCreateUserModal from "../../components/admin/showCreateUserModal"; +import ManageUserTable from "../../components/admin/users/ManageUserTable"; +import showCreateUserModal from "../../components/admin/users/showCreateUserModal"; import Meta from "../../components/Meta"; +import useConfig from "../../hooks/config.hook"; import userService from "../../services/user.service"; import User from "../../types/user.type"; import toast from "../../utils/toast.util"; @@ -12,6 +13,8 @@ import toast from "../../utils/toast.util"; const Users = () => { const [users, setUsers] = useState([]); const [isLoading, setIsLoading] = useState(true); + + const config = useConfig(); const modals = useModals(); const getUsers = () => { @@ -54,7 +57,9 @@ const Users = () => { User management