0
Fork 0
mirror of https://github.com/stonith404/pingvin-share.git synced 2025-02-19 01:55:48 -05:00

feat: invite new user with email

This commit is contained in:
Elias Schneider 2023-02-21 08:51:04 +01:00
parent 759c55f625
commit f9840505b8
No known key found for this signature in database
GPG key ID: 07E623B294202B6C
10 changed files with 111 additions and 29 deletions

View file

@ -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",

View file

@ -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({

View file

@ -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<CreateUserDTO>) {
return plainToClass(CreateUserDTO, partial, {
excludeExtraneousValues: true,

View file

@ -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],
})

View file

@ -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: {

View file

@ -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 = ({

View file

@ -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: <Title order={5}>Create user</Title>,
children: <Body modals={modals} getUsers={getUsers} />,
children: (
<Body modals={modals} smtpEnabled={smtpEnabled} getUsers={getUsers} />
),
});
};
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 = ({
<Stack>
<TextInput label="Username" {...form.getInputProps("username")} />
<TextInput label="Email" {...form.getInputProps("email")} />
<PasswordInput
label="New password"
{...form.getInputProps("password")}
/>
{smtpEnabled && (
<Switch
mt="xs"
labelPosition="left"
label="Set password manually"
description="If not checked, the user will receive an email with a link to set their password."
{...form.getInputProps("setPasswordManually", {
type: "checkbox",
})}
/>
)}
{form.values.setPasswordManually || !smtpEnabled && (
<PasswordInput
label="Password"
{...form.getInputProps("password")}
/>
)}
<Switch
styles={{
body: {
display: "flex",
justifyContent: "space-between",
},
}}
mt="xs"
labelPosition="left"
label="Admin privileges"
description="If checked, the user will be able to access the admin panel."
{...form.getInputProps("isAdmin", { type: "checkbox" })}
/>
<Group position="right">

View file

@ -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 = ({
</form>
<Accordion>
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
<Accordion.Control>Change password</Accordion.Control>
<Accordion.Control px={0}>Change password</Accordion.Control>
<Accordion.Panel>
<form
onSubmit={passwordForm.onSubmit(async (values) => {

View file

@ -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<User[]>([]);
const [isLoading, setIsLoading] = useState(true);
const config = useConfig();
const modals = useModals();
const getUsers = () => {
@ -54,7 +57,9 @@ const Users = () => {
User management
</Title>
<Button
onClick={() => showCreateUserModal(modals, getUsers)}
onClick={() =>
showCreateUserModal(modals, config.get("SMTP_ENABLED"), getUsers)
}
leftIcon={<TbPlus size={20} />}
>
Create

View file

@ -9,7 +9,7 @@ type User = {
export type CreateUser = {
username: string;
email: string;
password: string;
password?: string;
isAdmin?: boolean;
};