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:
parent
759c55f625
commit
f9840505b8
10 changed files with 111 additions and 29 deletions
|
@ -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 🐧",
|
"Hey!\nYou requested a password reset. Click this link to reset your password: {url}\nThe link expires in a hour.\nPingvin Share 🐧",
|
||||||
category: "email",
|
category: "email",
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
order: 13,
|
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",
|
key: "SMTP_ENABLED",
|
||||||
description:
|
description:
|
||||||
"Whether SMTP is enabled. Only set this to true if you entered the host, port, email, user and password of your SMTP server.",
|
"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,
|
secret: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 14,
|
order: 16,
|
||||||
key: "SMTP_HOST",
|
key: "SMTP_HOST",
|
||||||
description: "Host of the SMTP server",
|
description: "Host of the SMTP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -155,7 +173,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
||||||
category: "smtp",
|
category: "smtp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 15,
|
order: 17,
|
||||||
key: "SMTP_PORT",
|
key: "SMTP_PORT",
|
||||||
description: "Port of the SMTP server",
|
description: "Port of the SMTP server",
|
||||||
type: "number",
|
type: "number",
|
||||||
|
@ -163,7 +181,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
||||||
category: "smtp",
|
category: "smtp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 16,
|
order: 18,
|
||||||
key: "SMTP_EMAIL",
|
key: "SMTP_EMAIL",
|
||||||
description: "Email address which the emails get sent from",
|
description: "Email address which the emails get sent from",
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -171,7 +189,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
||||||
category: "smtp",
|
category: "smtp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 17,
|
order: 19,
|
||||||
key: "SMTP_USERNAME",
|
key: "SMTP_USERNAME",
|
||||||
description: "Username of the SMTP server",
|
description: "Username of the SMTP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
|
@ -179,7 +197,7 @@ const configVariables: Prisma.ConfigCreateInput[] = [
|
||||||
category: "smtp",
|
category: "smtp",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
order: 18,
|
order: 20,
|
||||||
key: "SMTP_PASSWORD",
|
key: "SMTP_PASSWORD",
|
||||||
description: "Password of the SMTP server",
|
description: "Password of the SMTP server",
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|
|
@ -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) {
|
async sendTestMail(recipientEmail: string) {
|
||||||
try {
|
try {
|
||||||
await this.getTransporter().sendMail({
|
await this.getTransporter().sendMail({
|
||||||
|
|
|
@ -1,12 +1,15 @@
|
||||||
import { Expose, plainToClass } from "class-transformer";
|
import { plainToClass } from "class-transformer";
|
||||||
import { Allow } from "class-validator";
|
import { Allow, IsOptional, MinLength } from "class-validator";
|
||||||
import { UserDTO } from "./user.dto";
|
import { UserDTO } from "./user.dto";
|
||||||
|
|
||||||
export class CreateUserDTO extends UserDTO {
|
export class CreateUserDTO extends UserDTO {
|
||||||
@Expose()
|
|
||||||
@Allow()
|
@Allow()
|
||||||
isAdmin: boolean;
|
isAdmin: boolean;
|
||||||
|
|
||||||
|
@MinLength(8)
|
||||||
|
@IsOptional()
|
||||||
|
password: string;
|
||||||
|
|
||||||
from(partial: Partial<CreateUserDTO>) {
|
from(partial: Partial<CreateUserDTO>) {
|
||||||
return plainToClass(CreateUserDTO, partial, {
|
return plainToClass(CreateUserDTO, partial, {
|
||||||
excludeExtraneousValues: true,
|
excludeExtraneousValues: true,
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import { Module } from "@nestjs/common";
|
import { Module } from "@nestjs/common";
|
||||||
|
import { EmailModule } from "src/email/email.module";
|
||||||
import { UserController } from "./user.controller";
|
import { UserController } from "./user.controller";
|
||||||
import { UserSevice } from "./user.service";
|
import { UserSevice } from "./user.service";
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
|
imports:[EmailModule],
|
||||||
providers: [UserSevice],
|
providers: [UserSevice],
|
||||||
controllers: [UserController],
|
controllers: [UserController],
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,13 +1,17 @@
|
||||||
import { BadRequestException, Injectable } from "@nestjs/common";
|
import { BadRequestException, Injectable } from "@nestjs/common";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
import { PrismaClientKnownRequestError } from "@prisma/client/runtime";
|
||||||
import * as argon from "argon2";
|
import * as argon from "argon2";
|
||||||
|
import { EmailService } from "src/email/email.service";
|
||||||
import { PrismaService } from "src/prisma/prisma.service";
|
import { PrismaService } from "src/prisma/prisma.service";
|
||||||
import { CreateUserDTO } from "./dto/createUser.dto";
|
import { CreateUserDTO } from "./dto/createUser.dto";
|
||||||
import { UpdateUserDto } from "./dto/updateUser.dto";
|
import { UpdateUserDto } from "./dto/updateUser.dto";
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class UserSevice {
|
export class UserSevice {
|
||||||
constructor(private prisma: PrismaService) {}
|
constructor(
|
||||||
|
private prisma: PrismaService,
|
||||||
|
private emailService: EmailService
|
||||||
|
) {}
|
||||||
|
|
||||||
async list() {
|
async list() {
|
||||||
return await this.prisma.user.findMany();
|
return await this.prisma.user.findMany();
|
||||||
|
@ -18,7 +22,17 @@ export class UserSevice {
|
||||||
}
|
}
|
||||||
|
|
||||||
async create(dto: CreateUserDTO) {
|
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 {
|
try {
|
||||||
return await this.prisma.user.create({
|
return await this.prisma.user.create({
|
||||||
data: {
|
data: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import { ActionIcon, Box, Group, Skeleton, Table } from "@mantine/core";
|
import { ActionIcon, Box, Group, Skeleton, Table } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
import { TbCheck, TbEdit, TbTrash } from "react-icons/tb";
|
||||||
import User from "../../types/user.type";
|
import User from "../../../types/user.type";
|
||||||
import showUpdateUserModal from "./showUpdateUserModal";
|
import showUpdateUserModal from "./showUpdateUserModal";
|
||||||
|
|
||||||
const ManageUserTable = ({
|
const ManageUserTable = ({
|
|
@ -10,38 +10,44 @@ import {
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import userService from "../../services/user.service";
|
import userService from "../../../services/user.service";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
const showCreateUserModal = (
|
const showCreateUserModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
|
smtpEnabled: boolean,
|
||||||
getUsers: () => void
|
getUsers: () => void
|
||||||
) => {
|
) => {
|
||||||
return modals.openModal({
|
return modals.openModal({
|
||||||
title: <Title order={5}>Create user</Title>,
|
title: <Title order={5}>Create user</Title>,
|
||||||
children: <Body modals={modals} getUsers={getUsers} />,
|
children: (
|
||||||
|
<Body modals={modals} smtpEnabled={smtpEnabled} getUsers={getUsers} />
|
||||||
|
),
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const Body = ({
|
const Body = ({
|
||||||
modals,
|
modals,
|
||||||
|
smtpEnabled,
|
||||||
getUsers,
|
getUsers,
|
||||||
}: {
|
}: {
|
||||||
modals: ModalsContextProps;
|
modals: ModalsContextProps;
|
||||||
|
smtpEnabled: boolean;
|
||||||
getUsers: () => void;
|
getUsers: () => void;
|
||||||
}) => {
|
}) => {
|
||||||
const form = useForm({
|
const form = useForm({
|
||||||
initialValues: {
|
initialValues: {
|
||||||
username: "",
|
username: "",
|
||||||
email: "",
|
email: "",
|
||||||
password: "",
|
password: undefined,
|
||||||
isAdmin: false,
|
isAdmin: false,
|
||||||
|
setPasswordManually: false,
|
||||||
},
|
},
|
||||||
validate: yupResolver(
|
validate: yupResolver(
|
||||||
yup.object().shape({
|
yup.object().shape({
|
||||||
email: yup.string().email(),
|
email: yup.string().email(),
|
||||||
username: yup.string().min(3),
|
username: yup.string().min(3),
|
||||||
password: yup.string().min(8),
|
password: yup.string().min(8).optional(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
@ -62,14 +68,34 @@ const Body = ({
|
||||||
<Stack>
|
<Stack>
|
||||||
<TextInput label="Username" {...form.getInputProps("username")} />
|
<TextInput label="Username" {...form.getInputProps("username")} />
|
||||||
<TextInput label="Email" {...form.getInputProps("email")} />
|
<TextInput label="Email" {...form.getInputProps("email")} />
|
||||||
<PasswordInput
|
{smtpEnabled && (
|
||||||
label="New password"
|
<Switch
|
||||||
{...form.getInputProps("password")}
|
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
|
<Switch
|
||||||
|
styles={{
|
||||||
|
body: {
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
},
|
||||||
|
}}
|
||||||
mt="xs"
|
mt="xs"
|
||||||
labelPosition="left"
|
labelPosition="left"
|
||||||
label="Admin privileges"
|
label="Admin privileges"
|
||||||
|
description="If checked, the user will be able to access the admin panel."
|
||||||
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
{...form.getInputProps("isAdmin", { type: "checkbox" })}
|
||||||
/>
|
/>
|
||||||
<Group position="right">
|
<Group position="right">
|
|
@ -11,9 +11,9 @@ import {
|
||||||
import { useForm, yupResolver } from "@mantine/form";
|
import { useForm, yupResolver } from "@mantine/form";
|
||||||
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
import { ModalsContextProps } from "@mantine/modals/lib/context";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import userService from "../../services/user.service";
|
import userService from "../../../services/user.service";
|
||||||
import User from "../../types/user.type";
|
import User from "../../../types/user.type";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../../utils/toast.util";
|
||||||
|
|
||||||
const showUpdateUserModal = (
|
const showUpdateUserModal = (
|
||||||
modals: ModalsContextProps,
|
modals: ModalsContextProps,
|
||||||
|
@ -90,7 +90,7 @@ const Body = ({
|
||||||
</form>
|
</form>
|
||||||
<Accordion>
|
<Accordion>
|
||||||
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
|
<Accordion.Item sx={{ borderBottom: "none" }} value="changePassword">
|
||||||
<Accordion.Control>Change password</Accordion.Control>
|
<Accordion.Control px={0}>Change password</Accordion.Control>
|
||||||
<Accordion.Panel>
|
<Accordion.Panel>
|
||||||
<form
|
<form
|
||||||
onSubmit={passwordForm.onSubmit(async (values) => {
|
onSubmit={passwordForm.onSubmit(async (values) => {
|
|
@ -2,9 +2,10 @@ import { Button, Group, Space, Text, Title } from "@mantine/core";
|
||||||
import { useModals } from "@mantine/modals";
|
import { useModals } from "@mantine/modals";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { TbPlus } from "react-icons/tb";
|
import { TbPlus } from "react-icons/tb";
|
||||||
import ManageUserTable from "../../components/admin/ManageUserTable";
|
import ManageUserTable from "../../components/admin/users/ManageUserTable";
|
||||||
import showCreateUserModal from "../../components/admin/showCreateUserModal";
|
import showCreateUserModal from "../../components/admin/users/showCreateUserModal";
|
||||||
import Meta from "../../components/Meta";
|
import Meta from "../../components/Meta";
|
||||||
|
import useConfig from "../../hooks/config.hook";
|
||||||
import userService from "../../services/user.service";
|
import userService from "../../services/user.service";
|
||||||
import User from "../../types/user.type";
|
import User from "../../types/user.type";
|
||||||
import toast from "../../utils/toast.util";
|
import toast from "../../utils/toast.util";
|
||||||
|
@ -12,6 +13,8 @@ import toast from "../../utils/toast.util";
|
||||||
const Users = () => {
|
const Users = () => {
|
||||||
const [users, setUsers] = useState<User[]>([]);
|
const [users, setUsers] = useState<User[]>([]);
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
const [isLoading, setIsLoading] = useState(true);
|
||||||
|
|
||||||
|
const config = useConfig();
|
||||||
const modals = useModals();
|
const modals = useModals();
|
||||||
|
|
||||||
const getUsers = () => {
|
const getUsers = () => {
|
||||||
|
@ -54,7 +57,9 @@ const Users = () => {
|
||||||
User management
|
User management
|
||||||
</Title>
|
</Title>
|
||||||
<Button
|
<Button
|
||||||
onClick={() => showCreateUserModal(modals, getUsers)}
|
onClick={() =>
|
||||||
|
showCreateUserModal(modals, config.get("SMTP_ENABLED"), getUsers)
|
||||||
|
}
|
||||||
leftIcon={<TbPlus size={20} />}
|
leftIcon={<TbPlus size={20} />}
|
||||||
>
|
>
|
||||||
Create
|
Create
|
||||||
|
|
|
@ -9,7 +9,7 @@ type User = {
|
||||||
export type CreateUser = {
|
export type CreateUser = {
|
||||||
username: string;
|
username: string;
|
||||||
email: string;
|
email: string;
|
||||||
password: string;
|
password?: string;
|
||||||
isAdmin?: boolean;
|
isAdmin?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
Loading…
Add table
Reference in a new issue