mirror of
https://github.com/Shadowfita/docmost.git
synced 2025-11-12 15:52:32 +10:00
Implement password change endpoint
* move email templates to server
This commit is contained in:
@ -1,12 +1,24 @@
|
|||||||
import api from "@/lib/api-client";
|
import api from "@/lib/api-client";
|
||||||
import { ILogin, IRegister, ITokenResponse } from "@/features/auth/types/auth.types";
|
import {
|
||||||
|
IChangePassword,
|
||||||
|
ILogin,
|
||||||
|
IRegister,
|
||||||
|
ITokenResponse,
|
||||||
|
} from "@/features/auth/types/auth.types";
|
||||||
|
|
||||||
export async function login(data: ILogin): Promise<ITokenResponse>{
|
export async function login(data: ILogin): Promise<ITokenResponse> {
|
||||||
const req = await api.post<ITokenResponse>("/auth/login", data);
|
const req = await api.post<ITokenResponse>("/auth/login", data);
|
||||||
return req.data as ITokenResponse;
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function register(data: IRegister): Promise<ITokenResponse>{
|
export async function register(data: IRegister): Promise<ITokenResponse> {
|
||||||
const req = await api.post<ITokenResponse>("/auth/register", data);
|
const req = await api.post<ITokenResponse>("/auth/register", data);
|
||||||
return req.data as ITokenResponse;
|
return req.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function changePassword(
|
||||||
|
data: IChangePassword,
|
||||||
|
): Promise<IChangePassword> {
|
||||||
|
const req = await api.post<IChangePassword>("/auth/change-password", data);
|
||||||
|
return req.data;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,18 +1,23 @@
|
|||||||
export interface ILogin {
|
export interface ILogin {
|
||||||
email: string,
|
email: string;
|
||||||
password: string
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface IRegister {
|
export interface IRegister {
|
||||||
email: string,
|
email: string;
|
||||||
password: string
|
password: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITokens {
|
export interface ITokens {
|
||||||
accessToken: string,
|
accessToken: string;
|
||||||
refreshToken: string
|
refreshToken: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ITokenResponse {
|
export interface ITokenResponse {
|
||||||
tokens: ITokens
|
tokens: ITokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface IChangePassword {
|
||||||
|
oldPassword: string;
|
||||||
|
newPassword: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -27,15 +27,17 @@ export default function ChangeEmail() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/*
|
||||||
<Button onClick={open} variant="default">
|
<Button onClick={open} variant="default">
|
||||||
Change email
|
Change email
|
||||||
</Button>
|
</Button>
|
||||||
|
*/}
|
||||||
|
|
||||||
<Modal opened={opened} onClose={close} title="Change email" centered>
|
<Modal opened={opened} onClose={close} title="Change email" centered>
|
||||||
<Text mb="md">
|
<Text mb="md">
|
||||||
To change your email, you have to enter your password and new email.
|
To change your email, you have to enter your password and new email.
|
||||||
</Text>
|
</Text>
|
||||||
<ChangePasswordForm />
|
<ChangeEmailForm />
|
||||||
</Modal>
|
</Modal>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
@ -50,7 +52,7 @@ const formSchema = z.object({
|
|||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>;
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
function ChangePasswordForm() {
|
function ChangeEmailForm() {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { Button, Group, Text, Modal, PasswordInput } from '@mantine/core';
|
import { Button, Group, Text, Modal, PasswordInput } from "@mantine/core";
|
||||||
import * as z from 'zod';
|
import * as z from "zod";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useDisclosure } from '@mantine/hooks';
|
import { useDisclosure } from "@mantine/hooks";
|
||||||
import * as React from 'react';
|
import * as React from "react";
|
||||||
import { useForm, zodResolver } from '@mantine/form';
|
import { useForm, zodResolver } from "@mantine/form";
|
||||||
|
import { changePassword } from "@/features/auth/services/auth-service.ts";
|
||||||
|
import { notifications } from "@mantine/notifications";
|
||||||
|
|
||||||
export default function ChangePassword() {
|
export default function ChangePassword() {
|
||||||
const [opened, { open, close }] = useDisclosure(false);
|
const [opened, { open, close }] = useDisclosure(false);
|
||||||
@ -17,55 +19,72 @@ export default function ChangePassword() {
|
|||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button onClick={open} variant="default">Change password</Button>
|
<Button onClick={open} variant="default">
|
||||||
|
Change password
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Modal opened={opened} onClose={close} title="Change password" centered>
|
<Modal opened={opened} onClose={close} title="Change password" centered>
|
||||||
<Text mb="md">Your password must be a minimum of 8 characters.</Text>
|
<Text mb="md">Your password must be a minimum of 8 characters.</Text>
|
||||||
<ChangePasswordForm />
|
<ChangePasswordForm onClose={close} />
|
||||||
|
|
||||||
</Modal>
|
</Modal>
|
||||||
</Group>
|
</Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
current: z.string({ required_error: 'your current password is required' }).min(1),
|
oldPassword: z
|
||||||
password: z.string({ required_error: 'New password is required' }).min(8),
|
.string({ required_error: "your current password is required" })
|
||||||
confirm_password: z.string({ required_error: 'Password confirmation is required' }).min(8),
|
.min(8),
|
||||||
}).refine(data => data.password === data.confirm_password, {
|
newPassword: z.string({ required_error: "New password is required" }).min(8),
|
||||||
message: 'Your new password and confirmation does not match.',
|
|
||||||
path: ['confirm_password'],
|
|
||||||
});
|
});
|
||||||
|
|
||||||
type FormValues = z.infer<typeof formSchema>
|
type FormValues = z.infer<typeof formSchema>;
|
||||||
|
|
||||||
function ChangePasswordForm() {
|
interface ChangePasswordFormProps {
|
||||||
|
onClose?: () => void;
|
||||||
|
}
|
||||||
|
function ChangePasswordForm({ onClose }: ChangePasswordFormProps) {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const form = useForm<FormValues>({
|
const form = useForm<FormValues>({
|
||||||
validate: zodResolver(formSchema),
|
validate: zodResolver(formSchema),
|
||||||
initialValues: {
|
initialValues: {
|
||||||
current: '',
|
oldPassword: "",
|
||||||
password: '',
|
newPassword: "",
|
||||||
confirm_password: '',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
function handleSubmit(data: FormValues) {
|
async function handleSubmit(data: FormValues) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
console.log(data);
|
try {
|
||||||
|
await changePassword({
|
||||||
|
oldPassword: data.oldPassword,
|
||||||
|
newPassword: data.newPassword,
|
||||||
|
});
|
||||||
|
notifications.show({
|
||||||
|
message: "Password changed successfully",
|
||||||
|
});
|
||||||
|
|
||||||
|
onClose();
|
||||||
|
} catch (err) {
|
||||||
|
notifications.show({
|
||||||
|
message: `Error: ${err.response.data.message}`,
|
||||||
|
color: "red",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
label="Current password"
|
label="Current password"
|
||||||
name="current"
|
name="oldPassword"
|
||||||
placeholder="Enter your current password"
|
placeholder="Enter your current password"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mb="md"
|
mb="md"
|
||||||
{...form.getInputProps('current')}
|
data-autofocus
|
||||||
|
{...form.getInputProps("oldPassword")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PasswordInput
|
<PasswordInput
|
||||||
@ -73,7 +92,7 @@ function ChangePasswordForm() {
|
|||||||
placeholder="Enter your new password"
|
placeholder="Enter your new password"
|
||||||
variant="filled"
|
variant="filled"
|
||||||
mb="md"
|
mb="md"
|
||||||
{...form.getInputProps('password')}
|
{...form.getInputProps("newPassword")}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||||
|
|||||||
@ -12,23 +12,25 @@
|
|||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --watch",
|
"start:debug": "nest start --debug --watch",
|
||||||
"start:prod": "node dist/main",
|
"start:prod": "node dist/main",
|
||||||
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
"email:dev": "email dev -p 5019 -d ./src/integrations/transactional/emails",
|
||||||
"test": "jest",
|
|
||||||
"test:watch": "jest --watch",
|
|
||||||
"test:cov": "jest --coverage",
|
|
||||||
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
|
||||||
"test:e2e": "jest --config test/jest-e2e.json",
|
|
||||||
"migration:create": "tsx ./src/kysely/migrate.ts create",
|
"migration:create": "tsx ./src/kysely/migrate.ts create",
|
||||||
"migration:up": "tsx ./src/kysely/migrate.ts up",
|
"migration:up": "tsx ./src/kysely/migrate.ts up",
|
||||||
"migration:down": "tsx ./src/kysely/migrate.ts down",
|
"migration:down": "tsx ./src/kysely/migrate.ts down",
|
||||||
"migration:latest": "tsx ./src/kysely/migrate.ts latest",
|
"migration:latest": "tsx ./src/kysely/migrate.ts latest",
|
||||||
"migration:redo": "tsx ./src/kysely/migrate.ts redo",
|
"migration:redo": "tsx ./src/kysely/migrate.ts redo",
|
||||||
"migration:codegen": "kysely-codegen --dialect=postgres --camel-case --env-file=../../.env --out-file=./src/kysely/types/db.d.ts"
|
"migration:codegen": "kysely-codegen --dialect=postgres --camel-case --env-file=../../.env --out-file=./src/kysely/types/db.d.ts",
|
||||||
|
"lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
|
||||||
|
"test": "jest",
|
||||||
|
"test:watch": "jest --watch",
|
||||||
|
"test:cov": "jest --coverage",
|
||||||
|
"test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
|
||||||
|
"test:e2e": "jest --config test/jest-e2e.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "^3.565.0",
|
"@aws-sdk/client-s3": "^3.565.0",
|
||||||
"@aws-sdk/s3-request-presigner": "^3.565.0",
|
"@aws-sdk/s3-request-presigner": "^3.565.0",
|
||||||
"@casl/ability": "^6.7.1",
|
"@casl/ability": "^6.7.1",
|
||||||
|
"@docmost/transactional": "workspace:^",
|
||||||
"@fastify/multipart": "^8.2.0",
|
"@fastify/multipart": "^8.2.0",
|
||||||
"@fastify/static": "^7.0.3",
|
"@fastify/static": "^7.0.3",
|
||||||
"@nestjs/bullmq": "^10.1.1",
|
"@nestjs/bullmq": "^10.1.1",
|
||||||
@ -42,6 +44,7 @@
|
|||||||
"@nestjs/platform-socket.io": "^10.3.8",
|
"@nestjs/platform-socket.io": "^10.3.8",
|
||||||
"@nestjs/serve-static": "^4.0.2",
|
"@nestjs/serve-static": "^4.0.2",
|
||||||
"@nestjs/websockets": "^10.3.8",
|
"@nestjs/websockets": "^10.3.8",
|
||||||
|
"@react-email/components": "0.0.17",
|
||||||
"@react-email/render": "^0.0.13",
|
"@react-email/render": "^0.0.13",
|
||||||
"@types/pg": "^8.11.5",
|
"@types/pg": "^8.11.5",
|
||||||
"bcrypt": "^5.1.1",
|
"bcrypt": "^5.1.1",
|
||||||
@ -93,6 +96,7 @@
|
|||||||
"jest": "^29.7.0",
|
"jest": "^29.7.0",
|
||||||
"kysely-codegen": "^0.15.0",
|
"kysely-codegen": "^0.15.0",
|
||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
|
"react-email": "^2.1.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
"ts-jest": "^29.1.2",
|
"ts-jest": "^29.1.2",
|
||||||
|
|||||||
@ -14,6 +14,11 @@ import { CreateUserDto } from './dto/create-user.dto';
|
|||||||
import { SetupGuard } from './guards/setup.guard';
|
import { SetupGuard } from './guards/setup.guard';
|
||||||
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||||
import { CreateAdminUserDto } from './dto/create-admin-user.dto';
|
import { CreateAdminUserDto } from './dto/create-admin-user.dto';
|
||||||
|
import { ChangePasswordDto } from './dto/change-password.dto';
|
||||||
|
import { AuthUser } from '../../decorators/auth-user.decorator';
|
||||||
|
import { User, Workspace } from '@docmost/db/types/entity.types';
|
||||||
|
import { AuthWorkspace } from '../../decorators/auth-workspace.decorator';
|
||||||
|
import { JwtAuthGuard } from '../../guards/jwt-auth.guard';
|
||||||
|
|
||||||
@Controller('auth')
|
@Controller('auth')
|
||||||
export class AuthController {
|
export class AuthController {
|
||||||
@ -44,4 +49,15 @@ export class AuthController {
|
|||||||
if (this.environmentService.isCloud()) throw new NotFoundException();
|
if (this.environmentService.isCloud()) throw new NotFoundException();
|
||||||
return this.authService.setup(createAdminUserDto);
|
return this.authService.setup(createAdminUserDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@UseGuards(JwtAuthGuard)
|
||||||
|
@HttpCode(HttpStatus.OK)
|
||||||
|
@Post('change-password')
|
||||||
|
async changePassword(
|
||||||
|
@Body() dto: ChangePasswordDto,
|
||||||
|
@AuthUser() user: User,
|
||||||
|
@AuthWorkspace() workspace: Workspace,
|
||||||
|
) {
|
||||||
|
return this.authService.changePassword(dto, user.id, workspace.id);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,8 +7,6 @@ import { TokenService } from './services/token.service';
|
|||||||
import { JwtStrategy } from './strategies/jwt.strategy';
|
import { JwtStrategy } from './strategies/jwt.strategy';
|
||||||
import { WorkspaceModule } from '../workspace/workspace.module';
|
import { WorkspaceModule } from '../workspace/workspace.module';
|
||||||
import { SignupService } from './services/signup.service';
|
import { SignupService } from './services/signup.service';
|
||||||
import { UserModule } from '../user/user.module';
|
|
||||||
import { SpaceModule } from '../space/space.module';
|
|
||||||
import { GroupModule } from '../group/group.module';
|
import { GroupModule } from '../group/group.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -23,10 +21,8 @@ import { GroupModule } from '../group/group.module';
|
|||||||
};
|
};
|
||||||
},
|
},
|
||||||
inject: [EnvironmentService],
|
inject: [EnvironmentService],
|
||||||
} as any),
|
}),
|
||||||
UserModule,
|
|
||||||
WorkspaceModule,
|
WorkspaceModule,
|
||||||
SpaceModule,
|
|
||||||
GroupModule,
|
GroupModule,
|
||||||
],
|
],
|
||||||
controllers: [AuthController],
|
controllers: [AuthController],
|
||||||
|
|||||||
13
apps/server/src/core/auth/dto/change-password.dto.ts
Normal file
13
apps/server/src/core/auth/dto/change-password.dto.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { IsNotEmpty, IsString, MinLength } from 'class-validator';
|
||||||
|
|
||||||
|
export class ChangePasswordDto {
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(8)
|
||||||
|
@IsString()
|
||||||
|
oldPassword: string;
|
||||||
|
|
||||||
|
@IsNotEmpty()
|
||||||
|
@MinLength(8)
|
||||||
|
@IsString()
|
||||||
|
newPassword: string;
|
||||||
|
}
|
||||||
@ -1,4 +1,9 @@
|
|||||||
import { Injectable, UnauthorizedException } from '@nestjs/common';
|
import {
|
||||||
|
BadRequestException,
|
||||||
|
Injectable,
|
||||||
|
NotFoundException,
|
||||||
|
UnauthorizedException,
|
||||||
|
} from '@nestjs/common';
|
||||||
import { LoginDto } from '../dto/login.dto';
|
import { LoginDto } from '../dto/login.dto';
|
||||||
import { CreateUserDto } from '../dto/create-user.dto';
|
import { CreateUserDto } from '../dto/create-user.dto';
|
||||||
import { TokenService } from './token.service';
|
import { TokenService } from './token.service';
|
||||||
@ -6,7 +11,10 @@ import { TokensDto } from '../dto/tokens.dto';
|
|||||||
import { SignupService } from './signup.service';
|
import { SignupService } from './signup.service';
|
||||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||||
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
import { UserRepo } from '@docmost/db/repos/user/user.repo';
|
||||||
import { comparePasswordHash } from '../../../helpers/utils';
|
import { comparePasswordHash, hashPassword } from '../../../helpers';
|
||||||
|
import { ChangePasswordDto } from '../dto/change-password.dto';
|
||||||
|
import { MailService } from '../../../integrations/mail/mail.service';
|
||||||
|
import ChangePasswordEmail from '@docmost/transactional/emails/change-password-email';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@ -14,6 +22,7 @@ export class AuthService {
|
|||||||
private signupService: SignupService,
|
private signupService: SignupService,
|
||||||
private tokenService: TokenService,
|
private tokenService: TokenService,
|
||||||
private userRepo: UserRepo,
|
private userRepo: UserRepo,
|
||||||
|
private mailService: MailService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async login(loginDto: LoginDto, workspaceId: string) {
|
async login(loginDto: LoginDto, workspaceId: string) {
|
||||||
@ -52,4 +61,43 @@ export class AuthService {
|
|||||||
|
|
||||||
return { tokens };
|
return { tokens };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async changePassword(
|
||||||
|
dto: ChangePasswordDto,
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
): Promise<void> {
|
||||||
|
const user = await this.userRepo.findById(userId, workspaceId, {
|
||||||
|
includePassword: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('User not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
const comparePasswords = await comparePasswordHash(
|
||||||
|
dto.oldPassword,
|
||||||
|
user.password,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!comparePasswords) {
|
||||||
|
throw new BadRequestException('Current password is incorrect');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPasswordHash = await hashPassword(dto.newPassword);
|
||||||
|
await this.userRepo.updateUser(
|
||||||
|
{
|
||||||
|
password: newPasswordHash,
|
||||||
|
},
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
);
|
||||||
|
|
||||||
|
const emailTemplate = ChangePasswordEmail({ username: user.name });
|
||||||
|
await this.mailService.sendToQueue({
|
||||||
|
to: user.email,
|
||||||
|
subject: 'Your password has been changed',
|
||||||
|
template: emailTemplate,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -100,12 +100,9 @@ export class GroupUserService {
|
|||||||
this.db,
|
this.db,
|
||||||
async (trx) => {
|
async (trx) => {
|
||||||
await this.groupService.findAndValidateGroup(groupId, workspaceId);
|
await this.groupService.findAndValidateGroup(groupId, workspaceId);
|
||||||
const user = await this.userRepo.findById(
|
const user = await this.userRepo.findById(userId, workspaceId, {
|
||||||
userId,
|
trx: trx,
|
||||||
workspaceId,
|
});
|
||||||
false,
|
|
||||||
trx,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotFoundException('User not found');
|
throw new NotFoundException('User not found');
|
||||||
|
|||||||
@ -4,4 +4,5 @@ export interface MailMessage {
|
|||||||
subject: string;
|
subject: string;
|
||||||
text?: string;
|
text?: string;
|
||||||
html?: string;
|
html?: string;
|
||||||
|
template?: any;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import { EnvironmentService } from '../environment/environment.service';
|
|||||||
import { InjectQueue } from '@nestjs/bullmq';
|
import { InjectQueue } from '@nestjs/bullmq';
|
||||||
import { QueueName, QueueJob } from '../queue/constants';
|
import { QueueName, QueueJob } from '../queue/constants';
|
||||||
import { Queue } from 'bullmq';
|
import { Queue } from 'bullmq';
|
||||||
|
import { render } from '@react-email/render';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MailService {
|
export class MailService {
|
||||||
@ -16,11 +17,21 @@ export class MailService {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
async sendEmail(message: MailMessage): Promise<void> {
|
async sendEmail(message: MailMessage): Promise<void> {
|
||||||
|
if (message.template) {
|
||||||
|
// in case this method is used directly
|
||||||
|
message.html = render(message.template);
|
||||||
|
}
|
||||||
|
|
||||||
const sender = `${this.environmentService.getMailFromName()} <${this.environmentService.getMailFromAddress()}> `;
|
const sender = `${this.environmentService.getMailFromName()} <${this.environmentService.getMailFromAddress()}> `;
|
||||||
await this.mailDriver.sendMail({ from: sender, ...message });
|
await this.mailDriver.sendMail({ from: sender, ...message });
|
||||||
}
|
}
|
||||||
|
|
||||||
async sendToQueue(message: MailMessage): Promise<void> {
|
async sendToQueue(message: MailMessage): Promise<void> {
|
||||||
|
if (message.template) {
|
||||||
|
// transform the React object because it gets lost when sent via the queue
|
||||||
|
message.html = render(message.template);
|
||||||
|
delete message.template;
|
||||||
|
}
|
||||||
await this.emailQueue.add(QueueJob.SEND_EMAIL, message);
|
await this.emailQueue.add(QueueJob.SEND_EMAIL, message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,53 +1,53 @@
|
|||||||
export const fontFamily = "HelveticaNeue,Helvetica,Arial,sans-serif";
|
export const fontFamily = 'HelveticaNeue,Helvetica,Arial,sans-serif';
|
||||||
|
|
||||||
export const main = {
|
export const main = {
|
||||||
backgroundColor: "#edf2f7",
|
backgroundColor: '#edf2f7',
|
||||||
fontFamily,
|
fontFamily,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const container = {
|
export const container = {
|
||||||
maxWidth: "580px",
|
maxWidth: '580px',
|
||||||
margin: "30px auto",
|
margin: '10px auto',
|
||||||
backgroundColor: "#ffffff",
|
backgroundColor: '#ffffff',
|
||||||
borderColor: "#e8e5ef",
|
borderColor: '#e8e5ef',
|
||||||
borderRadius: "2px",
|
borderRadius: '2px',
|
||||||
borderWidth: "1px",
|
borderWidth: '1px',
|
||||||
boxShadow: "0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015)",
|
boxShadow: '0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015)',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const content = {
|
export const content = {
|
||||||
padding: "5px 20px 10px 20px",
|
padding: '5px 20px 10px 20px',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const paragraph = {
|
export const paragraph = {
|
||||||
fontFamily:
|
fontFamily:
|
||||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||||
color: "#333",
|
color: '#333',
|
||||||
lineHeight: 1.5,
|
lineHeight: 1.5,
|
||||||
fontSize: 14,
|
fontSize: 14,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const h1 = {
|
export const h1 = {
|
||||||
color: "#333",
|
color: '#333',
|
||||||
fontFamily:
|
fontFamily:
|
||||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||||
fontSize: "24px",
|
fontSize: '24px',
|
||||||
fontWeight: "bold",
|
fontWeight: 'bold',
|
||||||
padding: "0",
|
padding: '0',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const logo = {
|
export const logo = {
|
||||||
display: "flex",
|
display: 'flex',
|
||||||
justifyContent: "center",
|
justifyContent: 'center',
|
||||||
alingItems: "center",
|
alingItems: 'center',
|
||||||
padding: 4,
|
padding: 4,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const link = {
|
export const link = {
|
||||||
textDecoration: "underline",
|
textDecoration: 'underline',
|
||||||
};
|
};
|
||||||
|
|
||||||
export const footer = {
|
export const footer = {
|
||||||
maxWidth: "580px",
|
maxWidth: '580px',
|
||||||
margin: "0 auto",
|
margin: '0 auto',
|
||||||
};
|
};
|
||||||
@ -0,0 +1,23 @@
|
|||||||
|
import { Section, Text } from '@react-email/components';
|
||||||
|
import * as React from 'react';
|
||||||
|
import { content, paragraph } from '../css/styles';
|
||||||
|
import { MailBody } from '../partials/partials';
|
||||||
|
|
||||||
|
interface ChangePasswordEmailProps {
|
||||||
|
username?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChangePasswordEmail = ({ username }: ChangePasswordEmailProps) => {
|
||||||
|
return (
|
||||||
|
<MailBody>
|
||||||
|
<Section style={content}>
|
||||||
|
<Text style={paragraph}>Hi {username},</Text>
|
||||||
|
<Text style={paragraph}>
|
||||||
|
This is a confirmation that your password has been changed.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</MailBody>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChangePasswordEmail;
|
||||||
@ -1,23 +1,28 @@
|
|||||||
import { footer, h1, logo, main } from "../css/styles";
|
import { container, footer, h1, logo, main } from '../css/styles';
|
||||||
import {
|
import {
|
||||||
Body,
|
Body,
|
||||||
|
Container,
|
||||||
Head,
|
Head,
|
||||||
Heading,
|
|
||||||
Html,
|
Html,
|
||||||
Row,
|
Row,
|
||||||
Section,
|
Section,
|
||||||
Text,
|
Text,
|
||||||
} from "@react-email/components";
|
} from '@react-email/components';
|
||||||
import * as React from "react";
|
import * as React from 'react';
|
||||||
|
|
||||||
interface MailBodyProps {
|
interface MailBodyProps {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MailBody({ children }: MailBodyProps) {
|
export function MailBody({ children }: MailBodyProps) {
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
<Body style={main}>{children}</Body>
|
<Body style={main}>
|
||||||
|
<MailHeader />
|
||||||
|
<Container style={container}>{children}</Container>
|
||||||
|
<MailFooter />
|
||||||
|
</Body>
|
||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -25,7 +30,7 @@ export function MailBody({ children }: MailBodyProps) {
|
|||||||
export function MailHeader() {
|
export function MailHeader() {
|
||||||
return (
|
return (
|
||||||
<Section style={logo}>
|
<Section style={logo}>
|
||||||
<Heading style={h1}>logo/text</Heading>
|
{/* <Heading style={h1}>docmost</Heading> */}
|
||||||
</Section>
|
</Section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -34,7 +39,7 @@ export function MailFooter() {
|
|||||||
return (
|
return (
|
||||||
<Section style={footer}>
|
<Section style={footer}>
|
||||||
<Row>
|
<Row>
|
||||||
<Text style={{ textAlign: "center", color: "#706a7b" }}>
|
<Text style={{ textAlign: 'center', color: '#706a7b' }}>
|
||||||
© {new Date().getFullYear()}, All Rights Reserved <br />
|
© {new Date().getFullYear()}, All Rights Reserved <br />
|
||||||
</Text>
|
</Text>
|
||||||
</Row>
|
</Row>
|
||||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
|||||||
import { InjectKysely } from 'nestjs-kysely';
|
import { InjectKysely } from 'nestjs-kysely';
|
||||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||||
import { Users } from '@docmost/db/types/db';
|
import { Users } from '@docmost/db/types/db';
|
||||||
import { hashPassword } from '../../../helpers/utils';
|
import { hashPassword } from '../../../helpers';
|
||||||
import { dbOrTx } from '@docmost/db/utils';
|
import { dbOrTx } from '@docmost/db/utils';
|
||||||
import {
|
import {
|
||||||
InsertableUser,
|
InsertableUser,
|
||||||
@ -35,14 +35,16 @@ export class UserRepo {
|
|||||||
async findById(
|
async findById(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
includePassword?: boolean,
|
opts?: {
|
||||||
trx?: KyselyTransaction,
|
includePassword?: boolean;
|
||||||
|
trx?: KyselyTransaction;
|
||||||
|
},
|
||||||
): Promise<User> {
|
): Promise<User> {
|
||||||
const db = dbOrTx(this.db, trx);
|
const db = dbOrTx(this.db, opts?.trx);
|
||||||
return db
|
return db
|
||||||
.selectFrom('users')
|
.selectFrom('users')
|
||||||
.select(this.baseFields)
|
.select(this.baseFields)
|
||||||
.$if(includePassword, (qb) => qb.select('password'))
|
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
||||||
.where('id', '=', userId)
|
.where('id', '=', userId)
|
||||||
.where('workspaceId', '=', workspaceId)
|
.where('workspaceId', '=', workspaceId)
|
||||||
.executeTakeFirst();
|
.executeTakeFirst();
|
||||||
|
|||||||
@ -22,6 +22,7 @@
|
|||||||
"paths": {
|
"paths": {
|
||||||
"@docmost/db": ["./src/kysely"],
|
"@docmost/db": ["./src/kysely"],
|
||||||
"@docmost/db/*": ["./src/kysely/*"],
|
"@docmost/db/*": ["./src/kysely/*"],
|
||||||
|
"@docmost/transactional/*": ["./src/integrations/transactional/*"]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
16
packages/transactional/.gitignore
vendored
16
packages/transactional/.gitignore
vendored
@ -1,16 +0,0 @@
|
|||||||
.env
|
|
||||||
# compiled output
|
|
||||||
/dist
|
|
||||||
/node_modules
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
# OS
|
|
||||||
.DS_Store
|
|
||||||
@ -1,26 +0,0 @@
|
|||||||
import { Container, Section, Text } from "@react-email/components";
|
|
||||||
import * as React from "react";
|
|
||||||
import { container, content, paragraph } from "../css/styles";
|
|
||||||
import { MailBody, MailFooter } from "../partials/partials";
|
|
||||||
|
|
||||||
interface WelcomeEmailProps {
|
|
||||||
username?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const TestEmail = ({ username }: WelcomeEmailProps) => {
|
|
||||||
return (
|
|
||||||
<MailBody>
|
|
||||||
<Container style={container}>
|
|
||||||
<Section style={content}>
|
|
||||||
<Text style={paragraph}>Hi {username},</Text>
|
|
||||||
<Text style={paragraph}>
|
|
||||||
This is a test email. Make sure to read it.
|
|
||||||
</Text>
|
|
||||||
</Section>
|
|
||||||
</Container>
|
|
||||||
<MailFooter />
|
|
||||||
</MailBody>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default TestEmail;
|
|
||||||
@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "@docmost/transactional",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "email dev -p 5019"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@react-email/components": "0.0.17",
|
|
||||||
"react-email": "^2.1.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
355
pnpm-lock.yaml
generated
355
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user