mirror of
https://github.com/docmost/docmost.git
synced 2025-11-10 05:02:06 +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 { 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);
|
||||
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);
|
||||
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 {
|
||||
email: string,
|
||||
password: string
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface IRegister {
|
||||
email: string,
|
||||
password: string
|
||||
email: string;
|
||||
password: string;
|
||||
}
|
||||
|
||||
export interface ITokens {
|
||||
accessToken: string,
|
||||
refreshToken: string
|
||||
accessToken: string;
|
||||
refreshToken: string;
|
||||
}
|
||||
|
||||
export interface ITokenResponse {
|
||||
tokens: ITokens
|
||||
tokens: ITokens;
|
||||
}
|
||||
|
||||
export interface IChangePassword {
|
||||
oldPassword: string;
|
||||
newPassword: string;
|
||||
}
|
||||
|
||||
@ -27,15 +27,17 @@ export default function ChangeEmail() {
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
{/*
|
||||
<Button onClick={open} variant="default">
|
||||
Change email
|
||||
</Button>
|
||||
*/}
|
||||
|
||||
<Modal opened={opened} onClose={close} title="Change email" centered>
|
||||
<Text mb="md">
|
||||
To change your email, you have to enter your password and new email.
|
||||
</Text>
|
||||
<ChangePasswordForm />
|
||||
<ChangeEmailForm />
|
||||
</Modal>
|
||||
</Group>
|
||||
);
|
||||
@ -50,7 +52,7 @@ const formSchema = z.object({
|
||||
|
||||
type FormValues = z.infer<typeof formSchema>;
|
||||
|
||||
function ChangePasswordForm() {
|
||||
function ChangeEmailForm() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const form = useForm<FormValues>({
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { Button, Group, Text, Modal, PasswordInput } from '@mantine/core';
|
||||
import * as z from 'zod';
|
||||
import { useState } from 'react';
|
||||
import { useDisclosure } from '@mantine/hooks';
|
||||
import * as React from 'react';
|
||||
import { useForm, zodResolver } from '@mantine/form';
|
||||
import { Button, Group, Text, Modal, PasswordInput } from "@mantine/core";
|
||||
import * as z from "zod";
|
||||
import { useState } from "react";
|
||||
import { useDisclosure } from "@mantine/hooks";
|
||||
import * as React from "react";
|
||||
import { useForm, zodResolver } from "@mantine/form";
|
||||
import { changePassword } from "@/features/auth/services/auth-service.ts";
|
||||
import { notifications } from "@mantine/notifications";
|
||||
|
||||
export default function ChangePassword() {
|
||||
const [opened, { open, close }] = useDisclosure(false);
|
||||
@ -17,55 +19,72 @@ export default function ChangePassword() {
|
||||
</Text>
|
||||
</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>
|
||||
<Text mb="md">Your password must be a minimum of 8 characters.</Text>
|
||||
<ChangePasswordForm />
|
||||
|
||||
<ChangePasswordForm onClose={close} />
|
||||
</Modal>
|
||||
</Group>
|
||||
);
|
||||
}
|
||||
|
||||
const formSchema = z.object({
|
||||
current: z.string({ required_error: 'your current password is required' }).min(1),
|
||||
password: z.string({ required_error: 'New password is required' }).min(8),
|
||||
confirm_password: z.string({ required_error: 'Password confirmation is required' }).min(8),
|
||||
}).refine(data => data.password === data.confirm_password, {
|
||||
message: 'Your new password and confirmation does not match.',
|
||||
path: ['confirm_password'],
|
||||
oldPassword: z
|
||||
.string({ required_error: "your current password is required" })
|
||||
.min(8),
|
||||
newPassword: z.string({ required_error: "New password is required" }).min(8),
|
||||
});
|
||||
|
||||
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 form = useForm<FormValues>({
|
||||
validate: zodResolver(formSchema),
|
||||
initialValues: {
|
||||
current: '',
|
||||
password: '',
|
||||
confirm_password: '',
|
||||
oldPassword: "",
|
||||
newPassword: "",
|
||||
},
|
||||
});
|
||||
|
||||
function handleSubmit(data: FormValues) {
|
||||
async function handleSubmit(data: FormValues) {
|
||||
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 (
|
||||
<form onSubmit={form.onSubmit(handleSubmit)}>
|
||||
|
||||
<PasswordInput
|
||||
label="Current password"
|
||||
name="current"
|
||||
name="oldPassword"
|
||||
placeholder="Enter your current password"
|
||||
variant="filled"
|
||||
mb="md"
|
||||
{...form.getInputProps('current')}
|
||||
data-autofocus
|
||||
{...form.getInputProps("oldPassword")}
|
||||
/>
|
||||
|
||||
<PasswordInput
|
||||
@ -73,7 +92,7 @@ function ChangePasswordForm() {
|
||||
placeholder="Enter your new password"
|
||||
variant="filled"
|
||||
mb="md"
|
||||
{...form.getInputProps('password')}
|
||||
{...form.getInputProps("newPassword")}
|
||||
/>
|
||||
|
||||
<Button type="submit" disabled={isLoading} loading={isLoading}>
|
||||
|
||||
@ -12,23 +12,25 @@
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main",
|
||||
"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",
|
||||
"email:dev": "email dev -p 5019 -d ./src/integrations/transactional/emails",
|
||||
"migration:create": "tsx ./src/kysely/migrate.ts create",
|
||||
"migration:up": "tsx ./src/kysely/migrate.ts up",
|
||||
"migration:down": "tsx ./src/kysely/migrate.ts down",
|
||||
"migration:latest": "tsx ./src/kysely/migrate.ts latest",
|
||||
"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": {
|
||||
"@aws-sdk/client-s3": "^3.565.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.565.0",
|
||||
"@casl/ability": "^6.7.1",
|
||||
"@docmost/transactional": "workspace:^",
|
||||
"@fastify/multipart": "^8.2.0",
|
||||
"@fastify/static": "^7.0.3",
|
||||
"@nestjs/bullmq": "^10.1.1",
|
||||
@ -42,6 +44,7 @@
|
||||
"@nestjs/platform-socket.io": "^10.3.8",
|
||||
"@nestjs/serve-static": "^4.0.2",
|
||||
"@nestjs/websockets": "^10.3.8",
|
||||
"@react-email/components": "0.0.17",
|
||||
"@react-email/render": "^0.0.13",
|
||||
"@types/pg": "^8.11.5",
|
||||
"bcrypt": "^5.1.1",
|
||||
@ -93,6 +96,7 @@
|
||||
"jest": "^29.7.0",
|
||||
"kysely-codegen": "^0.15.0",
|
||||
"prettier": "^3.2.5",
|
||||
"react-email": "^2.1.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.1.2",
|
||||
|
||||
@ -14,6 +14,11 @@ import { CreateUserDto } from './dto/create-user.dto';
|
||||
import { SetupGuard } from './guards/setup.guard';
|
||||
import { EnvironmentService } from '../../integrations/environment/environment.service';
|
||||
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')
|
||||
export class AuthController {
|
||||
@ -44,4 +49,15 @@ export class AuthController {
|
||||
if (this.environmentService.isCloud()) throw new NotFoundException();
|
||||
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 { WorkspaceModule } from '../workspace/workspace.module';
|
||||
import { SignupService } from './services/signup.service';
|
||||
import { UserModule } from '../user/user.module';
|
||||
import { SpaceModule } from '../space/space.module';
|
||||
import { GroupModule } from '../group/group.module';
|
||||
|
||||
@Module({
|
||||
@ -23,10 +21,8 @@ import { GroupModule } from '../group/group.module';
|
||||
};
|
||||
},
|
||||
inject: [EnvironmentService],
|
||||
} as any),
|
||||
UserModule,
|
||||
}),
|
||||
WorkspaceModule,
|
||||
SpaceModule,
|
||||
GroupModule,
|
||||
],
|
||||
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 { CreateUserDto } from '../dto/create-user.dto';
|
||||
import { TokenService } from './token.service';
|
||||
@ -6,7 +11,10 @@ import { TokensDto } from '../dto/tokens.dto';
|
||||
import { SignupService } from './signup.service';
|
||||
import { CreateAdminUserDto } from '../dto/create-admin-user.dto';
|
||||
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()
|
||||
export class AuthService {
|
||||
@ -14,6 +22,7 @@ export class AuthService {
|
||||
private signupService: SignupService,
|
||||
private tokenService: TokenService,
|
||||
private userRepo: UserRepo,
|
||||
private mailService: MailService,
|
||||
) {}
|
||||
|
||||
async login(loginDto: LoginDto, workspaceId: string) {
|
||||
@ -52,4 +61,43 @@ export class AuthService {
|
||||
|
||||
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,
|
||||
async (trx) => {
|
||||
await this.groupService.findAndValidateGroup(groupId, workspaceId);
|
||||
const user = await this.userRepo.findById(
|
||||
userId,
|
||||
workspaceId,
|
||||
false,
|
||||
trx,
|
||||
);
|
||||
const user = await this.userRepo.findById(userId, workspaceId, {
|
||||
trx: trx,
|
||||
});
|
||||
|
||||
if (!user) {
|
||||
throw new NotFoundException('User not found');
|
||||
|
||||
@ -4,4 +4,5 @@ export interface MailMessage {
|
||||
subject: string;
|
||||
text?: string;
|
||||
html?: string;
|
||||
template?: any;
|
||||
}
|
||||
|
||||
@ -6,6 +6,7 @@ import { EnvironmentService } from '../environment/environment.service';
|
||||
import { InjectQueue } from '@nestjs/bullmq';
|
||||
import { QueueName, QueueJob } from '../queue/constants';
|
||||
import { Queue } from 'bullmq';
|
||||
import { render } from '@react-email/render';
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
@ -16,11 +17,21 @@ export class MailService {
|
||||
) {}
|
||||
|
||||
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()}> `;
|
||||
await this.mailDriver.sendMail({ from: sender, ...message });
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,53 +1,53 @@
|
||||
export const fontFamily = "HelveticaNeue,Helvetica,Arial,sans-serif";
|
||||
export const fontFamily = 'HelveticaNeue,Helvetica,Arial,sans-serif';
|
||||
|
||||
export const main = {
|
||||
backgroundColor: "#edf2f7",
|
||||
backgroundColor: '#edf2f7',
|
||||
fontFamily,
|
||||
};
|
||||
|
||||
export const container = {
|
||||
maxWidth: "580px",
|
||||
margin: "30px auto",
|
||||
backgroundColor: "#ffffff",
|
||||
borderColor: "#e8e5ef",
|
||||
borderRadius: "2px",
|
||||
borderWidth: "1px",
|
||||
boxShadow: "0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015)",
|
||||
maxWidth: '580px',
|
||||
margin: '10px auto',
|
||||
backgroundColor: '#ffffff',
|
||||
borderColor: '#e8e5ef',
|
||||
borderRadius: '2px',
|
||||
borderWidth: '1px',
|
||||
boxShadow: '0 2px 0 rgba(0, 0, 150, 0.025), 2px 4px 0 rgba(0, 0, 150, 0.015)',
|
||||
};
|
||||
|
||||
export const content = {
|
||||
padding: "5px 20px 10px 20px",
|
||||
padding: '5px 20px 10px 20px',
|
||||
};
|
||||
|
||||
export const paragraph = {
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
color: "#333",
|
||||
color: '#333',
|
||||
lineHeight: 1.5,
|
||||
fontSize: 14,
|
||||
};
|
||||
|
||||
export const h1 = {
|
||||
color: "#333",
|
||||
color: '#333',
|
||||
fontFamily:
|
||||
"-apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif",
|
||||
fontSize: "24px",
|
||||
fontWeight: "bold",
|
||||
padding: "0",
|
||||
fontSize: '24px',
|
||||
fontWeight: 'bold',
|
||||
padding: '0',
|
||||
};
|
||||
|
||||
export const logo = {
|
||||
display: "flex",
|
||||
justifyContent: "center",
|
||||
alingItems: "center",
|
||||
display: 'flex',
|
||||
justifyContent: 'center',
|
||||
alingItems: 'center',
|
||||
padding: 4,
|
||||
};
|
||||
|
||||
export const link = {
|
||||
textDecoration: "underline",
|
||||
textDecoration: 'underline',
|
||||
};
|
||||
|
||||
export const footer = {
|
||||
maxWidth: "580px",
|
||||
margin: "0 auto",
|
||||
maxWidth: '580px',
|
||||
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 {
|
||||
Body,
|
||||
Container,
|
||||
Head,
|
||||
Heading,
|
||||
Html,
|
||||
Row,
|
||||
Section,
|
||||
Text,
|
||||
} from "@react-email/components";
|
||||
import * as React from "react";
|
||||
} from '@react-email/components';
|
||||
import * as React from 'react';
|
||||
|
||||
interface MailBodyProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export function MailBody({ children }: MailBodyProps) {
|
||||
return (
|
||||
<Html>
|
||||
<Head />
|
||||
<Body style={main}>{children}</Body>
|
||||
<Body style={main}>
|
||||
<MailHeader />
|
||||
<Container style={container}>{children}</Container>
|
||||
<MailFooter />
|
||||
</Body>
|
||||
</Html>
|
||||
);
|
||||
}
|
||||
@ -25,7 +30,7 @@ export function MailBody({ children }: MailBodyProps) {
|
||||
export function MailHeader() {
|
||||
return (
|
||||
<Section style={logo}>
|
||||
<Heading style={h1}>logo/text</Heading>
|
||||
{/* <Heading style={h1}>docmost</Heading> */}
|
||||
</Section>
|
||||
);
|
||||
}
|
||||
@ -34,7 +39,7 @@ export function MailFooter() {
|
||||
return (
|
||||
<Section style={footer}>
|
||||
<Row>
|
||||
<Text style={{ textAlign: "center", color: "#706a7b" }}>
|
||||
<Text style={{ textAlign: 'center', color: '#706a7b' }}>
|
||||
© {new Date().getFullYear()}, All Rights Reserved <br />
|
||||
</Text>
|
||||
</Row>
|
||||
@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common';
|
||||
import { InjectKysely } from 'nestjs-kysely';
|
||||
import { KyselyDB, KyselyTransaction } from '@docmost/db/types/kysely.types';
|
||||
import { Users } from '@docmost/db/types/db';
|
||||
import { hashPassword } from '../../../helpers/utils';
|
||||
import { hashPassword } from '../../../helpers';
|
||||
import { dbOrTx } from '@docmost/db/utils';
|
||||
import {
|
||||
InsertableUser,
|
||||
@ -35,14 +35,16 @@ export class UserRepo {
|
||||
async findById(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
includePassword?: boolean,
|
||||
trx?: KyselyTransaction,
|
||||
opts?: {
|
||||
includePassword?: boolean;
|
||||
trx?: KyselyTransaction;
|
||||
},
|
||||
): Promise<User> {
|
||||
const db = dbOrTx(this.db, trx);
|
||||
const db = dbOrTx(this.db, opts?.trx);
|
||||
return db
|
||||
.selectFrom('users')
|
||||
.select(this.baseFields)
|
||||
.$if(includePassword, (qb) => qb.select('password'))
|
||||
.$if(opts?.includePassword, (qb) => qb.select('password'))
|
||||
.where('id', '=', userId)
|
||||
.where('workspaceId', '=', workspaceId)
|
||||
.executeTakeFirst();
|
||||
|
||||
@ -22,6 +22,7 @@
|
||||
"paths": {
|
||||
"@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