Skip to content

Commit

Permalink
feat(api): Add workspace removal notification email template (#476)
Browse files Browse the repository at this point in the history
Co-authored-by: Rajdip Bhattacharya <[email protected]>
Co-authored-by: muntaxir4 <[email protected]>
  • Loading branch information
3 people authored Dec 1, 2024
1 parent 9efbf2d commit 40b754f
Show file tree
Hide file tree
Showing 14 changed files with 4,342 additions and 3,691 deletions.
4 changes: 2 additions & 2 deletions apps/api/jest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@ export default {
testMatch: ['**/*.spec.ts'],
testPathIgnorePatterns: ['.*.e2e.spec.ts'],
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
moduleFileExtensions: ['ts', 'js', 'html'],
moduleFileExtensions: ['ts', 'js', 'html', 'tsx', 'jsx'],
coverageDirectory: '../../coverage/apps/api'
}
4 changes: 2 additions & 2 deletions apps/api/jest.e2e-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ export default {
testEnvironment: 'node',
testMatch: ['**/*.e2e.spec.ts'],
transform: {
'^.+\\.[tj]s$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
'^.+\\.[tj]sx?$': ['ts-jest', { tsconfig: '<rootDir>/tsconfig.spec.json' }]
},
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
moduleFileExtensions: ['ts', 'js', 'html'],
moduleFileExtensions: ['ts', 'js', 'html', 'tsx', 'jsx'],
coverageDirectory: '../../coverage/apps/api',
coverageReporters: ['json'],
collectCoverage: true
Expand Down
2 changes: 2 additions & 0 deletions apps/api/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions apps/api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,17 +34,22 @@
"@nestjs/swagger": "^7.3.0",
"@nestjs/throttler": "^6.2.1",
"@nestjs/websockets": "^10.3.7",
"@react-email/components": "^0.0.25",
"@react-email/preview": "0.0.11",
"@react-email/render": "^1.0.1",
"@socket.io/redis-adapter": "^8.3.0",
"@supabase/supabase-js": "^2.39.6",
"class-transformer": "^0.5.1",
"class-validator": "^0.14.1",
"cookie-parser": "^1.4.6",
"dayjs": "^1.11.11",
"eccrypto": "^1.1.6",
"minio": "^8.0.0",
"nodemailer": "^6.9.9",
"passport-github2": "^0.1.12",
"passport-gitlab2": "^5.0.0",
"passport-google-oauth20": "^2.0.0",
"react": "^18.3.1",
"redis": "^4.6.13",
"rxjs": "^7.8.1",
"socket.io": "^4.7.5",
Expand All @@ -59,10 +64,12 @@
"@types/eccrypto": "^1.1.6",
"@types/express": "^4.17.17",
"@types/multer": "^1.4.11",
"@types/react": "^18.3.12",
"@types/supertest": "^6.0.0",
"@types/uuid": "^9.0.8",
"ajv": "^7",
"dotenv-cli": "^7.4.2",
"file-loader": "^6.2.0",
"jest": "^29.5.0",
"jest-mock-extended": "^3.0.5",
"prettier": "^3.0.0",
Expand Down
151 changes: 151 additions & 0 deletions apps/api/src/mail/emails/workspace-removal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import * as React from 'react'
import {
Body,
Container,
Head,
Heading,
Html,
Link,
Preview,
Section,
Text
} from '@react-email/components'
import dayjs from 'dayjs'

interface WorkspaceRemovalEmailProps {
workspaceName: string
removedOn: string
}

export const RemovedFromWorkspaceEmail = ({
workspaceName,
removedOn
}: WorkspaceRemovalEmailProps) => {
const formattedRemovedOnDate = dayjs(removedOn).format(
'ddd, MMM D, YYYY h:mm A'
)

return (
<Html>
<Head />
<Preview>Removal from Workspace</Preview>
<Body style={main}>
<Container style={container}>
<Section style={content}>
<Heading style={h1}>Removal from Workspace</Heading>
<Text style={text}>Dear User,</Text>
<Text style={text}>
We hope this email finds you well. We are writing to inform you
that your access to the following workspace has been removed:
</Text>
<Section style={workspaceDetails}>
<Text style={workspaceInfo}>
<strong>Workspace Name:</strong> {workspaceName}
</Text>
<Text style={workspaceInfo}>
<strong>Removed On:</strong> {formattedRemovedOnDate}
</Text>
</Section>
<Text style={text}>
If you believe this action was taken in error or have any
questions regarding this change, please contact your project
administrator or our support team.
</Text>
<Text style={text}>
We appreciate your understanding and thank you for your
contributions to the project.
</Text>
<Text style={text}>
Cheers,
<br />
Team Keyshade
</Text>
</Section>
<Section style={footer}>
<Text style={footerText}>
This is an automated message. Please do not reply to this email.
</Text>
<Text style={footerText}>
Read our{' '}
<Link href="https://www.keyshade.xyz/privacy" style={link}>
Privacy Policy
</Link>{' '}
and{' '}
<Link
href="https://www.keyshade.xyz/terms_and_condition"
style={link}
>
Terms and Conditions
</Link>{' '}
for more information on how we manage your data and services.
</Text>
</Section>
</Container>
</Body>
</Html>
)
}

export default RemovedFromWorkspaceEmail

const main = {
fontFamily: "'Segoe UI', 'Roboto', sans-serif",
lineHeight: '1.6',
color: '#04050a',
backgroundColor: '#fafafa',
margin: '0',
padding: '20px'
}

const container = {
maxWidth: '600px',
margin: '0 auto',
backgroundColor: '#fff',
borderRadius: '5px',
boxShadow: '0 2px 8px rgba(0, 0, 0, 0.05)'
}

const content = {
padding: '20px 40px'
}

const h1 = {
color: '#000',
marginBottom: '20px',
fontSize: '24px',
fontWeight: '600'
}

const text = {
marginBottom: '5px',
color: '#666'
}

const workspaceDetails = {
width: '100%',
backgroundColor: '#fafafa',
borderRadius: '5px',
margin: '20px 0px',
padding: '10px 20px'
}

const workspaceInfo = {
margin: '7px 0px'
}

const footer = {
borderTop: '1px solid #eaeaea',
padding: '20px'
}

const footerText = {
fontSize: '12px',
color: '#999',
textAlign: 'center' as const,
margin: '0'
}

const link = {
color: '#000',
textDecoration: 'underline'
}
6 changes: 6 additions & 0 deletions apps/api/src/mail/services/interface.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,10 @@ export interface IMailService {
adminUserCreateEmail(email: string): Promise<void>

feedbackEmail(email: string, feedback: string): Promise<void>

removedFromWorkspace(
email: string,
workspaceName: string,
removedOn: Date
): Promise<void>
}
21 changes: 20 additions & 1 deletion apps/api/src/mail/services/mail.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
} from '@nestjs/common'
import { IMailService } from './interface.service'
import { Transporter, createTransport } from 'nodemailer'
import RemovedFromWorkspaceEmail from '../emails/workspace-removal'
import { render } from '@react-email/render'

@Injectable()
export class MailService implements IMailService {
Expand Down Expand Up @@ -133,7 +135,7 @@ export class MailService implements IMailService {
<p>keyshade Team</p>
</body>
`
await this.sendEmail(process.env.ADMIN_EMAIL, subject, body)
await this.sendEmail(process.env.ADMIN_EMAIL!, subject, body)
}

async feedbackEmail(email: string, feedback: string): Promise<void> {
Expand All @@ -158,6 +160,23 @@ export class MailService implements IMailService {
await this.sendEmail(email, subject, body)
}

async removedFromWorkspace(
email: string,
workspaceName: string,
removedOn: Date
): Promise<void> {
const subject = `Your access was revoked from ${workspaceName}`

const body = await render(
RemovedFromWorkspaceEmail({
removedOn: removedOn.toISOString(),
workspaceName
})
)

await this.sendEmail(email, subject, body)
}

private async sendEmail(
email: string,
subject: string,
Expand Down
10 changes: 10 additions & 0 deletions apps/api/src/mail/services/mock.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,4 +38,14 @@ export class MockMailService implements IMailService {
async sendEmailChangedOtp(email: string, otp: string): Promise<void> {
this.log.log(`Email change OTP for email ${email} is ${otp}`)
}

async removedFromWorkspace(
email: string,
workspaceName: string,
removedOn: Date
): Promise<void> {
this.log.log(
`User with email ${email} has been removed from the workspace ${workspaceName} on ${removedOn.toISOString()}`
)
}
}
29 changes: 15 additions & 14 deletions apps/api/src/socket/change-notifier.socket.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,25 +94,26 @@ export default class ChangeNotifier
)
@UseGuards(AuthGuard, ApiKeyGuard)
@SubscribeMessage('register-client-app')
/**
* This event is emitted from the CLI to register
* itself with our services so that it can receive live updates.
*
* The CLI will send a `ChangeNotifierRegistration` object
* as the message body, containing the workspace slug, project slug,
* and environment slug that the client app wants to receive updates for.
*
* We will then check if the user has access to the workspace,
* project, and environment, and if so, add the client to the
* list of connected clients for that environment.
*
* Finally, we will send an ACK to the client with a status code of 200.
*/
async handleRegister(
@ConnectedSocket() client: Socket,
@MessageBody() data: ChangeNotifierRegistration,
@CurrentUser() user: User
) {
/**
* This event is emitted from the CLI to register
* itself with our services so that it can receive live updates.
*
* The CLI will send a `ChangeNotifierRegistration` object
* as the message body, containing the workspace slug, project slug,
* and environment slug that the client app wants to receive updates for.
*
* We will then check if the user has access to the workspace,
* project, and environment, and if so, add the client to the
* list of connected clients for that environment.
*
* Finally, we will send an ACK to the client with a status code of 200.
*/

try {
// Check if the user has access to the workspace
await this.authorityCheckerService.checkAuthorityOverWorkspace({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,18 @@ export class WorkspaceMembershipService {
}
}
})

// Send an email to the removed users
const removedOn = new Date()
const emailPromises = userEmails.map((userEmail) =>
this.mailService.removedFromWorkspace(
userEmail,
workspace.name,
removedOn
)
)

await Promise.all(emailPromises)
}

await createEvent(
Expand Down
3 changes: 2 additions & 1 deletion apps/api/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"compilerOptions": {
"jsx": "react-jsx",
"module": "NodeNext",
"declaration": true,
"removeComments": true,
Expand All @@ -20,6 +21,6 @@
"moduleResolution": "NodeNext",
"paths": {
"@/*": ["./src/*"]
}
},
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,7 @@
"ts-jest": "^29.1.0",
"tsconfig": "workspace:*",
"tsconfig-paths": "^4.2.0",
"turbo": "^2.3.1"
"turbo": "^2.3.3"
},
"dependencies": {
"@keyshade/api-client": "workspace:*",
Expand Down
18 changes: 9 additions & 9 deletions packages/eslint-config-custom/package.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"name": "eslint-config-custom",
"license": "MIT",
"version": "0.0.0",
"private": true,
"devDependencies": {
"@vercel/style-guide": "^5.0.0",
"eslint-config-turbo": "^2.3.1",
"typescript": "^4.5.3"
}
"name": "eslint-config-custom",
"license": "MIT",
"version": "0.0.0",
"private": true,
"devDependencies": {
"@vercel/style-guide": "^5.0.0",
"eslint-config-turbo": "^2.3.1",
"typescript": "^4.5.3"
}
}
Loading

0 comments on commit 40b754f

Please sign in to comment.