Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(api-service): plain cards fetch user organizations #7268

Merged
merged 10 commits into from
Dec 23, 2024
2 changes: 2 additions & 0 deletions apps/api/src/.example.env
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ CLERK_LONG_LIVED_TOKEN=
TUNNEL_BASE_ADDRESS=
PLAIN_SUPPORT_KEY='PLAIN_SUPPORT_KEY'
PLAIN_IDENTITY_VERIFICATION_SECRET_KEY='PLAIN_IDENTITY_VERIFICATION_SECRET_KEY'
PLAIN_CARDS_HMAC_SECRET_KEY='PLAIN_CARDS_HMAC_SECRET_KEY'

NOVU_INTERNAL_SECRET_KEY=
# expressed in seconds or a string describing a time span [zeit/ms](https://github.com/zeit/ms.js). Eg: 60, "2 days", "10h", "7d"
SUBSCRIBER_WIDGET_JWT_EXPIRATION_TIME='15 days'
45 changes: 45 additions & 0 deletions apps/api/src/app/support/dto/plain-card.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { ApiProperty } from '@nestjs/swagger';

export class PlainCustomer {
@ApiProperty()
id: string;

@ApiProperty()
externalId?: string;

@ApiProperty()
email?: string;
}

export class PlainTenant {
@ApiProperty()
id?: string;

@ApiProperty()
externalId?: string;
}

export class PlainThread {
@ApiProperty()
id?: string;

@ApiProperty()
externalId?: string;
}

export class PlainCardRequestDto {
jainpawan21 marked this conversation as resolved.
Show resolved Hide resolved
@ApiProperty()
cardKeys?: string[];

@ApiProperty()
customer?: PlainCustomer | null;

@ApiProperty()
tenant?: PlainTenant | null;

@ApiProperty()
thread?: PlainThread | null;

@ApiProperty()
timestamp: string;
}
18 changes: 18 additions & 0 deletions apps/api/src/app/support/guards/plain-cards.guard.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { CanActivate, ExecutionContext, Injectable, UnauthorizedException } from '@nestjs/common';
import { Observable } from 'rxjs';
import crypto from 'node:crypto';

@Injectable()
export class PlainCardsGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean | Promise<boolean> | Observable<boolean> {
const request = context.switchToHttp().getRequest();

const requestBody = JSON.stringify(request.body);
const plainCardsHMACSecretKey = process.env.PLAIN_CARDS_HMAC_SECRET_KEY as string;
const incomingSignature = request.headers['plain-request-signature'];
jainpawan21 marked this conversation as resolved.
Show resolved Hide resolved
if (!incomingSignature) throw new UnauthorizedException('Plain request signature is missing');
const expectedSignature = crypto.createHmac('sha-256', plainCardsHMACSecretKey).update(requestBody).digest('hex');

return incomingSignature === expectedSignature;
}
}
125 changes: 11 additions & 114 deletions apps/api/src/app/support/support.controller.ts
Original file line number Diff line number Diff line change
@@ -1,127 +1,24 @@
import { Body, Controller, Post, UseGuards } from '@nestjs/common';
import { Body, Controller, Post, UseGuards, Request, Response, RawBodyRequest } from '@nestjs/common';
import { UserAuthGuard, UserSession } from '@novu/application-generic';
import { UserRepository } from '@novu/dal';
import { UserSessionData } from '@novu/shared';
import { CreateSupportThreadDto } from './dto/create-thread.dto';
import { CreateSupportThreadUsecase } from './usecases/create-thread.usecase';
import { CreateSupportThreadCommand } from './usecases/create-thread.command';
import { PlainCardRequestDto } from './dto/plain-card.dto';
import { PlainCardsCommand } from './usecases/plain-cards.command';
import { CreateSupportThreadUsecase, PlainCardsUsecase } from './usecases';
import { PlainCardsGuard } from './guards/plain-cards.guard';

@Controller('/support')
export class SupportController {
constructor(
private readonly userRepository: UserRepository,
private createSupportThreadUsecase: CreateSupportThreadUsecase
private createSupportThreadUsecase: CreateSupportThreadUsecase,
private plainCardsUsecase: PlainCardsUsecase
) {}

@Post('plain/cards')
async getPlainCards() {
return {
data: {},

cards: [
{
key: 'plain-customer-details',
components: [
{
componentSpacer: {
spacerSize: 'S',
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: 'Registered at',
textColor: 'MUTED',
},
},
],
rowAsideContent: [
{
componentText: {
text: '7/18/2024, 1:00 PM',
},
},
],
},
},
{
componentSpacer: {
spacerSize: 'M',
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: 'Last signed in',
textColor: 'MUTED',
},
},
],
rowAsideContent: [
{
componentText: {
text: '10/20/2024, 12:57 PM',
},
},
],
},
},
{
componentSpacer: {
spacerSize: 'M',
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: 'Last device used',
textColor: 'MUTED',
},
},
],
rowAsideContent: [
{
componentText: {
text: 'iPhone 13 🍎',
},
},
],
},
},
{
componentSpacer: {
spacerSize: 'M',
},
},
{
componentRow: {
rowMainContent: [
{
componentText: {
text: 'Marketing preferences',
textColor: 'MUTED',
},
},
],
rowAsideContent: [
{
componentText: {
text: 'Opted out 🙅',
},
},
],
},
},
],
},
],
};
@UseGuards(PlainCardsGuard)
@Post('user-organizations')
async fetchUserOrganizations(@Body() body: PlainCardRequestDto) {
return this.plainCardsUsecase.fetchUserOrganizations(PlainCardsCommand.create({ ...body }));
}

@UseGuards(UserAuthGuard)
Expand Down
6 changes: 4 additions & 2 deletions apps/api/src/app/support/support.module.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import { Module } from '@nestjs/common';
import { SupportService } from '@novu/application-generic';
import { OrganizationRepository } from '@novu/dal';
import { SupportController } from './support.controller';
import { SharedModule } from '../shared/shared.module';
import { CreateSupportThreadUsecase } from './usecases/create-thread.usecase';
import { CreateSupportThreadUsecase, PlainCardsUsecase } from './usecases';
import { PlainCardsGuard } from './guards/plain-cards.guard';

@Module({
imports: [SharedModule],
controllers: [SupportController],
providers: [CreateSupportThreadUsecase, SupportService],
providers: [CreateSupportThreadUsecase, PlainCardsUsecase, SupportService, OrganizationRepository, PlainCardsGuard],
})
export class SupportModule {}
2 changes: 2 additions & 0 deletions apps/api/src/app/support/usecases/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './create-thread.usecase';
export * from './plain-cards.usecase';
22 changes: 22 additions & 0 deletions apps/api/src/app/support/usecases/plain-cards.command.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import { BaseCommand } from '@novu/application-generic';
import { IsArray, IsDefined, IsOptional, IsString } from 'class-validator';
import { PlainCustomer, PlainTenant, PlainThread } from '../dto/plain-card.dto';

export class PlainCardsCommand extends BaseCommand {
@IsOptional()
@IsArray()
cardKeys?: string[];

@IsOptional()
customer?: PlainCustomer | null;

@IsOptional()
tenant?: PlainTenant | null;

@IsOptional()
thread?: PlainThread | null;

@IsDefined()
@IsString()
timestamp: string;
}
Loading
Loading