feat: Simple nodemailer integration
This commit is contained in:
45
common/emails/auth.ts
Normal file
45
common/emails/auth.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
export const welcomeToApp =
|
||||
`
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Welcome to Research Shock</title>
|
||||
<style>
|
||||
body { font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; line-height: 1.6; color: #333; margin: 0; padding: 0; background-color: #f4f7f9; }
|
||||
.container { max-width: 600px; margin: 20px auto; background: #ffffff; padding: 40px; border-radius: 8px; box-shadow: 0 4px 10px rgba(0,0,0,0.05); }
|
||||
.logo { font-size: 24px; font-weight: bold; color: #2563eb; margin-bottom: 20px; text-align: center; }
|
||||
h1 { font-size: 22px; color: #111827; }
|
||||
p { margin-bottom: 20px; color: #4b5563; }
|
||||
.button { display: inline-block; padding: 12px 24px; background-color: #2563eb; color: #ffffff; text-decoration: none; border-radius: 5px; font-weight: 600; transition: background 0.3s ease; }
|
||||
.footer { margin-top: 30px; font-size: 12px; color: #9ca3af; text-align: center; }
|
||||
.divider { height: 1px; background: #e5e7eb; margin: 30px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="logo">MultiTenant SaaS</div>
|
||||
<h1>Welcome to the team!</h1>
|
||||
<p>Hi there,</p>
|
||||
<p>Thanks for signing up for <strong>MultiTenant Saas</strong>. We're excited to help you around.</p>
|
||||
<p>To get started, we recommend setting up your organization profile and inviting your first team member.</p>
|
||||
|
||||
<div style="text-align: center; margin: 35px 0;">
|
||||
<a href="http://localhost:3000" class="button">Go to Dashboard</a>
|
||||
</div>
|
||||
|
||||
<p>If you have any questions, just reply to this email. We're here to help!</p>
|
||||
|
||||
<div class="divider"></div>
|
||||
|
||||
<p>Cheers,<br>Team</p>
|
||||
|
||||
<div class="footer">
|
||||
© 2026 App. All rights reserved.<br>
|
||||
If you didn't create an account, you can safely ignore this email.
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
`
|
||||
9
common/emails/index.ts
Normal file
9
common/emails/index.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { welcomeToApp } from "./auth"
|
||||
|
||||
const EmailTemplates = {
|
||||
welcomeToApp,
|
||||
}
|
||||
|
||||
export default EmailTemplates
|
||||
|
||||
|
||||
8
core/mail/mail.module.ts
Normal file
8
core/mail/mail.module.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { Module } from "@nestjs/common";
|
||||
import { MailService } from "./mail.service";
|
||||
|
||||
@Module({
|
||||
providers: [MailService],
|
||||
exports: [MailService]
|
||||
})
|
||||
export class MailModule { }
|
||||
45
core/mail/mail.service.ts
Normal file
45
core/mail/mail.service.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import * as nodemailer from "nodemailer";
|
||||
|
||||
@Injectable()
|
||||
export class MailService {
|
||||
private transporter: nodemailer.Transporter;
|
||||
private mailServiceAvailable = false;
|
||||
|
||||
constructor(private readonly configService: ConfigService) {
|
||||
const mailId = this.configService.get<string | undefined>("MAIL_ID");
|
||||
const mailPass = this.configService.get<string | undefined>("MAIL_PASS");
|
||||
|
||||
if (!mailId || !mailPass)
|
||||
throw new Error("Make sure MAIL_ID and MAIL_PASS environment variables are set")
|
||||
|
||||
// Use secure in prod
|
||||
// TODO: A table for failed emails to retry later(actually bullmq)
|
||||
this.transporter = nodemailer.createTransport({
|
||||
host: "smtp.gmail.com",
|
||||
port: 587,
|
||||
secure: false, // Use true for port 465, false for port 587
|
||||
auth: {
|
||||
user: mailId,
|
||||
pass: mailPass,
|
||||
},
|
||||
from: mailId
|
||||
});
|
||||
|
||||
this.mailServiceAvailable = true;
|
||||
}
|
||||
|
||||
sendMail({ to, subject, body }: { to: string, subject: string, body: string }) {
|
||||
if (!this.mailServiceAvailable)
|
||||
throw new Error("Mail service not available")
|
||||
|
||||
this.transporter.sendMail(
|
||||
{
|
||||
to,
|
||||
subject,
|
||||
html: body
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -37,6 +37,7 @@
|
||||
"cache-manager": "^7.2.8",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.3",
|
||||
"nodemailer": "^8.0.1",
|
||||
"pg": "^8.18.0",
|
||||
"reflect-metadata": "^0.2.2",
|
||||
"rxjs": "^7.8.1"
|
||||
@@ -54,6 +55,7 @@
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^29.5.14",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/nodemailer": "^7.0.11",
|
||||
"@types/pg": "^8.16.0",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"eslint": "^9.18.0",
|
||||
|
||||
30
pnpm-lock.yaml
generated
30
pnpm-lock.yaml
generated
@@ -47,6 +47,9 @@ importers:
|
||||
class-validator:
|
||||
specifier: ^0.14.3
|
||||
version: 0.14.3
|
||||
nodemailer:
|
||||
specifier: ^8.0.1
|
||||
version: 8.0.1
|
||||
pg:
|
||||
specifier: ^8.18.0
|
||||
version: 8.18.0
|
||||
@@ -93,6 +96,9 @@ importers:
|
||||
'@types/node':
|
||||
specifier: ^22.10.7
|
||||
version: 22.19.10
|
||||
'@types/nodemailer':
|
||||
specifier: ^7.0.11
|
||||
version: 7.0.11
|
||||
'@types/pg':
|
||||
specifier: ^8.16.0
|
||||
version: 8.16.0
|
||||
@@ -751,42 +757,49 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/nice-linux-arm64-musl@1.1.1':
|
||||
resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/nice-linux-ppc64-gnu@1.1.1':
|
||||
resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/nice-linux-riscv64-gnu@1.1.1':
|
||||
resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/nice-linux-s390x-gnu@1.1.1':
|
||||
resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/nice-linux-x64-gnu@1.1.1':
|
||||
resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@napi-rs/nice-linux-x64-musl@1.1.1':
|
||||
resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@napi-rs/nice-openharmony-arm64@1.1.1':
|
||||
resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==}
|
||||
@@ -1082,24 +1095,28 @@ packages:
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-arm64-musl@1.15.11':
|
||||
resolution: {integrity: sha512-PYftgsTaGnfDK4m6/dty9ryK1FbLk+LosDJ/RJR2nkXGc8rd+WenXIlvHjWULiBVnS1RsjHHOXmTS4nDhe0v0w==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-linux-x64-gnu@1.15.11':
|
||||
resolution: {integrity: sha512-DKtnJKIHiZdARyTKiX7zdRjiDS1KihkQWatQiCHMv+zc2sfwb4Glrodx2VLOX4rsa92NLR0Sw8WLcPEMFY1szQ==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@swc/core-linux-x64-musl@1.15.11':
|
||||
resolution: {integrity: sha512-mUjjntHj4+8WBaiDe5UwRNHuEzLjIWBTSGTw0JT9+C9/Yyuh4KQqlcEQ3ro6GkHmBGXBFpGIj/o5VMyRWfVfWw==}
|
||||
engines: {node: '>=10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@swc/core-win32-arm64-msvc@1.15.11':
|
||||
resolution: {integrity: sha512-ZkNNG5zL49YpaFzfl6fskNOSxtcZ5uOYmWBkY4wVAvgbSAQzLRVBp+xArGWh2oXlY/WgL99zQSGTv7RI5E6nzA==}
|
||||
@@ -1236,6 +1253,9 @@ packages:
|
||||
'@types/node@22.19.10':
|
||||
resolution: {integrity: sha512-tF5VOugLS/EuDlTBijk0MqABfP8UxgYazTLo3uIn3b4yJgg26QRbVYJYsDtHrjdDUIRfP70+VfhTTc+CE1yskw==}
|
||||
|
||||
'@types/nodemailer@7.0.11':
|
||||
resolution: {integrity: sha512-E+U4RzR2dKrx+u3N4DlsmLaDC6mMZOM/TPROxA0UAPiTgI0y4CEFBmZE+coGWTjakDriRsXG368lNk1u9Q0a2g==}
|
||||
|
||||
'@types/pg@8.16.0':
|
||||
resolution: {integrity: sha512-RmhMd/wD+CF8Dfo+cVIy3RR5cl8CyfXQ0tGgW6XBL8L4LM/UTEbNXYRbLwU6w+CgrKBNbrQWt4FUtTfaU5jSYQ==}
|
||||
|
||||
@@ -2984,6 +3004,10 @@ packages:
|
||||
node-releases@2.0.27:
|
||||
resolution: {integrity: sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==}
|
||||
|
||||
nodemailer@8.0.1:
|
||||
resolution: {integrity: sha512-5kcldIXmaEjZcHR6F28IKGSgpmZHaF1IXLWFTG+Xh3S+Cce4MiakLtWY+PlBU69fLbRa8HlaGIrC/QolUpHkhg==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
normalize-path@3.0.0:
|
||||
resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -5141,6 +5165,10 @@ snapshots:
|
||||
dependencies:
|
||||
undici-types: 6.21.0
|
||||
|
||||
'@types/nodemailer@7.0.11':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
|
||||
'@types/pg@8.16.0':
|
||||
dependencies:
|
||||
'@types/node': 22.19.10
|
||||
@@ -7179,6 +7207,8 @@ snapshots:
|
||||
|
||||
node-releases@2.0.27: {}
|
||||
|
||||
nodemailer@8.0.1: {}
|
||||
|
||||
normalize-path@3.0.0: {}
|
||||
|
||||
normalize-url@8.1.1: {}
|
||||
|
||||
104
roadmap.md
Normal file
104
roadmap.md
Normal file
@@ -0,0 +1,104 @@
|
||||
```
|
||||
|
||||
``` md
|
||||
## Immediate
|
||||
|
||||
User registers: AuthService saves the user and emits a UserSignedUpEvent.
|
||||
|
||||
Listener catches it: The WelcomeEmailListener hears the event.
|
||||
|
||||
Queue handles it: The listener adds a job to BullMQ called send-welcome-email.
|
||||
|
||||
Worker processes it: A separate background worker picks up the job, tries to send the email, and automatically handles retries if it fails.
|
||||
```
|
||||
```
|
||||
|
||||
|
||||
# 🏗️ SaaS Architect’s Roadmap: NestJS & DevOps
|
||||
|
||||
This document serves as a strategic guide for evolving from a developer to an architect. It balances the "What" (the technology) with the "How" (the engineering mindset).
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Phase 1: The "What" — Advanced Backend Features
|
||||
*Focus: Scaling complexity and ensuring production-grade reliability.*
|
||||
|
||||
### 1. Advanced Multi-Tenancy Architecture
|
||||
* **The Goal:** Move beyond simple `organization_id` filters to true data isolation.
|
||||
* **Concepts:** Shared Database/Separate Schema or **Row Level Security (RLS)**.
|
||||
* **Challenge:** Implement a NestJS `Interceptor` or `Provider` that extracts a `tenant_id` from the request and automatically switches the database context/schema.
|
||||
|
||||
|
||||
### 2. Background Tasks & Reliability
|
||||
* **The Goal:** Decouple time-consuming tasks from the request/response cycle.
|
||||
* **Tech Stack:** **BullMQ** + **Redis**.
|
||||
* **Implementation:** Offload email invitations, image processing, and report generation to a separate "Worker" instance.
|
||||
|
||||
|
||||
### 3. Real-Time Interactions
|
||||
* **The Goal:** Create a reactive, living dashboard.
|
||||
* **Tech Stack:** **Socket.io** (WebSockets).
|
||||
* **Implementation:** Sync team membership changes or notification counts in real-time without requiring a page refresh.
|
||||
|
||||
### 4. Billing & Webhooks (The SaaS "S")
|
||||
* **The Goal:** Turn a project into a business.
|
||||
* **Tech Stack:** **Stripe API**.
|
||||
* **Learning Point:** Handle **Idempotency**. Ensure that if Stripe sends a "Payment Success" webhook twice, you only upgrade the user's account once.
|
||||
|
||||
---
|
||||
|
||||
## 🧠 Phase 2: The "How" — Architectural Thinking
|
||||
*Focus: Designing systems that don't break under edge cases.*
|
||||
|
||||
### 1. State Machine Logic
|
||||
Stop thinking in "CRUD" (Create, Read, Update, Delete) and start thinking in **Transitions**.
|
||||
* **Entity Lifecycle:** An Invitation isn't just a row; it's a flow: `Pending` → `Accepted` | `Declined` | `Expired`.
|
||||
* **Action:** Explicitly define what happens during every status change (e.g., "On Accept: Create User, Join Team, Clear Invite Cache").
|
||||
|
||||
|
||||
### 2. Atomic Transactions
|
||||
Never perform "partial" updates.
|
||||
* **Strategy:** Use Database Transactions (`BEGIN...COMMIT`) for any operation touching multiple tables. If the user joins the team but the invitation update fails, the whole thing must roll back.
|
||||
|
||||
### 3. Event-Driven Side Effects
|
||||
Don't bloat your services with 10 different tasks.
|
||||
* **Pattern:** Use an **Event Emitter**.
|
||||
* **Logic:** `UserService.create()` emits a `USER_CREATED` event. Separate listeners then handle the email, the analytics ping, and the Slack notification independently.
|
||||
|
||||
---
|
||||
|
||||
## ☁️ Phase 3: The DevOps Pillar
|
||||
*Focus: Automation, Observability, and "Infrastructure as Code".*
|
||||
|
||||
### 1. Observability (Stop Guessing)
|
||||
* **Structured Logging:** Use **Pino** to output JSON logs that include `trace_id` and `tenant_id`.
|
||||
* **Tracing:** Implement **OpenTelemetry** to visualize the lifecycle of a request across different services.
|
||||
|
||||
|
||||
### 2. Infrastructure as Code (IaC)
|
||||
* **The Goal:** Destroy and rebuild your entire cloud environment in minutes.
|
||||
* **Tech Stack:** **Terraform**.
|
||||
* **Action:** Codify your GCP/Azure VMs, VPC Firewalls, and Managed Databases so you never have to click buttons in a portal manually.
|
||||
|
||||
### 3. Hardened CI/CD
|
||||
* **The Goal:** Catch errors before they hit production.
|
||||
* **Action:** Add "Structure Verification" steps in your GitHub Actions to ensure the build folder matches what the server expects (e.g., checking for Next.js standalone chunks).
|
||||
|
||||
---
|
||||
|
||||
## 🤖 The "Anti-GIGO" AI Workflow
|
||||
*How to use AI as an architect, not a typist.*
|
||||
|
||||
1. **Sketch First:** Define your DB schema and logic flow on a whiteboard before opening the chat.
|
||||
2. **Hunt Edge Cases:** Ask yourself: "What if the user is already in another org? What if the database is locked?"
|
||||
3. **Prompt as an Architect:**
|
||||
* *Bad:* "Make an invite system."
|
||||
* *Good:* "Design a NestJS service for an Invitation State Machine. It must use a database transaction, handle BullMQ for email side-effects, and include a unique constraint on (email, organization_id)."
|
||||
4. **Audit the Output:** Check for N+1 query problems, lack of input validation, and missing error handling.
|
||||
|
||||
---
|
||||
|
||||
**Next Steps for You:**
|
||||
* Start with the **Invitation State Machine**.
|
||||
* Implement **BullMQ** for the invitation email.
|
||||
* Write your first **Terraform** script to manage your GCP firewall.
|
||||
@@ -14,6 +14,7 @@ import { OrganizationModule } from './organization/organization.module';
|
||||
import { OrganizationMembershipModule } from './organization-membership/organization-membership.module';
|
||||
import { AuthorizationModule } from './authorization/authorization.module';
|
||||
import { CacheModule } from './cache/cache.module';
|
||||
import { MailModule } from 'core/mail/mail.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -28,6 +29,7 @@ import { CacheModule } from './cache/cache.module';
|
||||
OrganizationMembershipModule,
|
||||
AuthorizationModule,
|
||||
CacheModule,
|
||||
MailModule
|
||||
],
|
||||
controllers: [AppController],
|
||||
providers: [
|
||||
|
||||
@@ -6,6 +6,7 @@ import { AuthGuard } from './guards/auth.guard';
|
||||
import { UserModule } from 'src/user/user.module';
|
||||
import { JwtModule } from '@nestjs/jwt';
|
||||
import { RequestContextModule } from 'core/als/request-context.module';
|
||||
import { MailModule } from 'core/mail/mail.module';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
@@ -17,6 +18,11 @@ import { RequestContextModule } from 'core/als/request-context.module';
|
||||
},
|
||||
],
|
||||
controllers: [AuthController],
|
||||
imports: [UserModule, JwtModule, RequestContextModule],
|
||||
imports: [
|
||||
UserModule,
|
||||
JwtModule,
|
||||
RequestContextModule,
|
||||
MailModule
|
||||
],
|
||||
})
|
||||
export class AuthModule {}
|
||||
export class AuthModule { }
|
||||
|
||||
@@ -5,6 +5,8 @@ import * as bcrypt from 'bcrypt';
|
||||
import { UserService } from 'src/user/user.service';
|
||||
import { TokenInputType } from './types';
|
||||
import { JwtService } from '@nestjs/jwt';
|
||||
import { MailService } from 'core/mail/mail.service';
|
||||
import EmailTemplates from 'common/emails';
|
||||
|
||||
@Injectable()
|
||||
@Public()
|
||||
@@ -12,7 +14,8 @@ export class AuthService {
|
||||
constructor(
|
||||
private readonly userService: UserService,
|
||||
private readonly jwtService: JwtService,
|
||||
) {}
|
||||
private readonly mailService: MailService,
|
||||
) { }
|
||||
|
||||
async register(dto: RegisterUserRequestDTO) {
|
||||
const hashedPassword = await bcrypt.hash(dto.password, 10);
|
||||
@@ -21,6 +24,11 @@ export class AuthService {
|
||||
password: hashedPassword,
|
||||
});
|
||||
|
||||
this.mailService.sendMail({
|
||||
to: dto.email,
|
||||
subject: "Welcome onboard",
|
||||
body: EmailTemplates.welcomeToApp
|
||||
})
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -50,9 +58,9 @@ export class AuthService {
|
||||
};
|
||||
}
|
||||
|
||||
logout() {}
|
||||
logout() { }
|
||||
|
||||
resetPassword() {}
|
||||
resetPassword() { }
|
||||
|
||||
// TODO: Use nest jwt
|
||||
private async genSignedTokens(token: TokenInputType) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Body, Controller, Delete, Get, Param, ParseEnumPipe, ParseUUIDPipe, Pat
|
||||
import { OrganizationMembershipService } from './organization-membership.service';
|
||||
import { RequestContextService } from 'core/als/request-context.service';
|
||||
import { ApiBearerAuth, ApiOperation, ApiParam, ApiQuery } from '@nestjs/swagger';
|
||||
import { InviteUserToOrganizationRequestDTO, JoinRequestToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO } from './dto';
|
||||
import { JoinRequestToOrganizationRequestDTO, UserOrganizationInvitationActionRequestDTO } from './dto';
|
||||
import { ORGANIZATION_JOIN_REQUEST_TYPE } from 'prisma/generated/prisma/enums';
|
||||
|
||||
/* NOTE: Regarding endpoint path naming
|
||||
|
||||
@@ -152,7 +152,7 @@ export class OrganizationMembershipService {
|
||||
})
|
||||
}
|
||||
catch (err) {
|
||||
throw new NotFoundException("Join request")
|
||||
throw new NotFoundException("Request")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user