Skip to content

Commit

Permalink
feat(server): add support for marketplace integration
Browse files Browse the repository at this point in the history
  • Loading branch information
overbit committed Jun 18, 2024
1 parent 24727bf commit 9d2f677
Show file tree
Hide file tree
Showing 11 changed files with 3,589 additions and 109 deletions.
3,240 changes: 3,155 additions & 85 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@apollo/client": "^3.7.1",
"@aws-sdk/client-codecommit": "^3.362.0",
"@aws-sdk/client-ecr": "^3.405.0",
"@aws-sdk/client-marketplace-metering": "^3.598.0",
"@codebrew/nestjs-storage": "^0.1.7",
"@emotion/react": "^11.10.5",
"@emotion/styled": "11.11.0",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
-- CreateTable
CREATE TABLE "AwsMarketplaceIntegration" (
"id" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"email" TEXT NOT NULL,
"productCode" TEXT NOT NULL,
"customerIdentifier" TEXT NOT NULL,
"awsAccountId" TEXT NOT NULL,
"accountId" TEXT,
"workspaceId" TEXT,

CONSTRAINT "AwsMarketplaceIntegration_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "AwsMarketplaceIntegration_email_key" ON "AwsMarketplaceIntegration"("email");

-- CreateIndex
CREATE UNIQUE INDEX "AwsMarketplaceIntegration_customerIdentifier_key" ON "AwsMarketplaceIntegration"("customerIdentifier");

-- CreateIndex
CREATE UNIQUE INDEX "AwsMarketplaceIntegration_accountId_key" ON "AwsMarketplaceIntegration"("accountId");

-- AddForeignKey
ALTER TABLE "AwsMarketplaceIntegration" ADD CONSTRAINT "AwsMarketplaceIntegration_accountId_fkey" FOREIGN KEY ("accountId") REFERENCES "Account"("id") ON DELETE CASCADE ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "AwsMarketplaceIntegration" ADD CONSTRAINT "AwsMarketplaceIntegration_workspaceId_fkey" FOREIGN KEY ("workspaceId") REFERENCES "Workspace"("id") ON DELETE SET NULL ON UPDATE CASCADE;
62 changes: 39 additions & 23 deletions packages/amplication-prisma-db/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -18,32 +18,34 @@ datasource db {
}

model Account {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique(map: "Account.email_unique")
firstName String
lastName String
password String
currentUserId String? @unique(map: "Account.currentUserId_unique")
githubId String?
currentUser User? @relation(fields: [currentUserId], references: [id])
users User[] @relation("AccountOnUser")
previewAccountEmail String?
previewAccountType PreviewAccountType @default(None)
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique(map: "Account.email_unique")
firstName String
lastName String
password String
currentUserId String? @unique(map: "Account.currentUserId_unique")
githubId String?
currentUser User? @relation(fields: [currentUserId], references: [id])
users User[] @relation("AccountOnUser")
previewAccountEmail String?
previewAccountType PreviewAccountType @default(None)
AwsMarketplaceIntegration AwsMarketplaceIntegration?
}

model Workspace {
id String @id(map: "Organization_pkey") @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique(map: "Workspace.name_unique")
allowLLMFeatures Boolean @default(true)
users User[]
invitations Invitation[]
subscriptions Subscription[]
gitOrganizations GitOrganization[]
projects Project[]
id String @id(map: "Organization_pkey") @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
name String @unique(map: "Workspace.name_unique")
allowLLMFeatures Boolean @default(true)
users User[]
invitations Invitation[]
subscriptions Subscription[]
gitOrganizations GitOrganization[]
projects Project[]
AwsMarketplaceIntegration AwsMarketplaceIntegration[]
}

model Project {
Expand Down Expand Up @@ -565,6 +567,20 @@ model Coupon {
couponType String?
}

model AwsMarketplaceIntegration {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
productCode String
customerIdentifier String @unique
awsAccountId String
accountId String? @unique
account Account? @relation(fields: [accountId], references: [id], onDelete: Cascade)
workspaceId String?
workspace Workspace? @relation(fields: [workspaceId], references: [id])
}

enum EnumSubscriptionPlan {
Free
Pro
Expand Down
4 changes: 4 additions & 0 deletions packages/amplication-server/.env
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,7 @@ CHAT_ASSISTANT_ID="CHAT_ASSISTANT_ID"
FEATURE_AI_ASSISTANT_ENABLED=true

PLUGIN_API_URL="https://plugin-api.amplication.com/graphql"

AWS_MARKETPLACE_INTEGRATION_ACCOUNT_ID=""
AWS_MARKETPLACE_INTEGRATION_KEY=""
AWS_MARKETPLACE_INTEGRATION_SECRET=""
30 changes: 29 additions & 1 deletion packages/amplication-server/src/core/auth/auth.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import { Env } from "../../env";
import { AmplicationLogger } from "@amplication/util/nestjs/logging";
import { AuthExceptionFilter } from "../../filters/auth-exception.filter";
import { requiresAuth } from "express-openid-connect";
import { AwsMarketplaceService } from "./aws-marketplace/aws-marketplace.service";
import { AWS_MARKETPLACE_INTEGRATION_CALLBACK_PATH } from "./aws-marketplace/constant";
export const AUTH_LOGIN_PATH = "/auth/login";
export const AUTH_LOGOUT_PATH = "/auth/logout";
export const AUTH_CALLBACK_PATH = "/auth/callback";
Expand All @@ -35,7 +37,8 @@ export class AuthController {
private readonly authService: AuthService,
@Inject(AmplicationLogger)
private readonly logger: AmplicationLogger,
private readonly configService: ConfigService
private readonly configService: ConfigService,
private readonly awsMarketplaceService: AwsMarketplaceService
) {
this.clientHost = configService.get(Env.CLIENT_HOST);
this.host = `${configService.get(Env.HOST)}`;
Expand Down Expand Up @@ -149,4 +152,29 @@ export class AuthController {

await this.authService.loginOrSignUp(profile, response);
}

@UseInterceptors(MorganInterceptor("combined"))
@Post("/auth/marketplace")
async awsMarketplace(@Req() request: Request, @Res() response: Response) {
const resBody =
await this.awsMarketplaceService.handleAwsMarketplaceRequest(
request,
response
);
response.send(resBody);
}

@UseInterceptors(MorganInterceptor("combined"))
@Post(AWS_MARKETPLACE_INTEGRATION_CALLBACK_PATH)
async awsMarketplaceCallback(
@Req() request: Request,
@Res() response: Response
) {
const res =
await this.awsMarketplaceService.handleAwsMarketplaceRegistration(
request,
response
);
response.send(res);
}
}
2 changes: 2 additions & 0 deletions packages/amplication-server/src/core/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { OpenIDConnectAuthMiddleware } from "./oidc.middleware";
import { SegmentAnalyticsModule } from "../../services/segmentAnalytics/segmentAnalytics.module";
import { IdpModule } from "../idp/idp.module";
import { PreviewUserService } from "./previewUser.service";
import { AwsMarketplaceService } from "./aws-marketplace/aws-marketplace.service";

@Module({
imports: [
Expand Down Expand Up @@ -79,6 +80,7 @@ import { PreviewUserService } from "./previewUser.service";
GitHubStrategyConfigService,
OpenIDConnectAuthMiddleware,
SegmentAnalyticsModule,
AwsMarketplaceService,
],
controllers: [AuthController],
exports: [GqlAuthGuard, AuthResolver, PreviewUserService],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { Inject, Injectable } from "@nestjs/common";
import {
MarketplaceMeteringClient,
ResolveCustomerCommand,
ResolveCustomerResult,
} from "@aws-sdk/client-marketplace-metering";
import { AmplicationLogger } from "@amplication/util/nestjs/logging";
import { Request, Response } from "express";
import { Env } from "../../../env";
import { PrismaService } from "../../../prisma";
import { ConfigService } from "@nestjs/config";
import { stringifyUrl } from "query-string";
import { URL } from "url";
import { registrationHtmlBody } from "./registration-page";
import { AWS_MARKETPLACE_INTEGRATION_CALLBACK_PATH } from "./constant";
import * as cookie from "cookie";
import { AuthService } from "../auth.service";

@Injectable()
export class AwsMarketplaceService {
private awsMeteringClient: MarketplaceMeteringClient;
private host: string;

private cookieName = "mpid";

constructor(
@Inject(AmplicationLogger) private readonly logger: AmplicationLogger,
configService: ConfigService,
private readonly prismaService: PrismaService,
private readonly authService: AuthService
) {
const config = {
credentials: {
accountId: configService.get(
Env.AWS_MARKETPLACE_INTEGRATION_ACCOUNT_ID
),
accessKeyId: configService.get(Env.AWS_MARKETPLACE_INTEGRATION_KEY),
secretAccessKey: configService.get(
Env.AWS_MARKETPLACE_INTEGRATION_SECRET
),
},
region: "us-east-1",
};

this.awsMeteringClient = new MarketplaceMeteringClient(config);

this.host = configService.get(Env.HOST);
}

private async resolveCustomer(token: string): Promise<ResolveCustomerResult> {
this.logger.debug(`Resolve aws marketplace customer`, { token });

const command = new ResolveCustomerCommand({
// eslint-disable-next-line @typescript-eslint/naming-convention
RegistrationToken: token,
});

try {
// eslint-disable-next-line @typescript-eslint/naming-convention
const result = await this.awsMeteringClient.send(command);

this.logger.debug(`Resolve aws marketplace customer response`, {
...result,
});
return result;
} catch (error) {
this.logger.error(`Failed to resolve aws marketplace customer`, error, {
token,
});
throw error;
}
}

async handleAwsMarketplaceRequest(
request: Request,
response: Response
): Promise<string> {
const awsToken = request.body["x-amzn-marketplace-token"];

const url = stringifyUrl({
url: new URL(
AWS_MARKETPLACE_INTEGRATION_CALLBACK_PATH,
this.host
).toString(),
});

const clientDomain = new URL(url).hostname;
const cookieDomainParts = clientDomain.split(".");
const cookieDomain = cookieDomainParts
.slice(Math.max(cookieDomainParts.length - 2, 0))
.join(".");
response.cookie(this.cookieName, awsToken, {
domain: cookieDomain,
secure: true,
httpOnly: true,
});

return registrationHtmlBody(url);
}

async handleAwsMarketplaceRegistration(
request: Request,
response: Response
): Promise<string> {
try {
const contactEmail = request.body["contactEmail"];

const reqCookies = cookie.parse(request.headers.cookie);

const awsToken = reqCookies[this.cookieName];
const customer = await this.resolveCustomer(awsToken);

const clientDomain = new URL(this.host).hostname;
const cookieDomainParts = clientDomain.split(".");
const cookieDomain = cookieDomainParts
.slice(Math.max(cookieDomainParts.length - 2, 0))
.join(".");

response.cookie(this.cookieName, awsToken, {
domain: cookieDomain,
secure: true,
httpOnly: true,
expires: new Date(),
});

const isRegistrationSuccessful =
await this.authService.signupWithBusinessEmail({
data: {
email: contactEmail,
},
});

if (!isRegistrationSuccessful) {
return `Failed to register. Please contact Amplication support and quote: ${customer.CustomerIdentifier} ${customer.ProductCode}`;
}

await this.prismaService.awsMarketplaceIntegration.upsert({
create: {
email: contactEmail,
awsAccountId: customer.CustomerAWSAccountId,
customerIdentifier: customer.CustomerIdentifier,
productCode: customer.ProductCode,
},
update: {
email: contactEmail,
awsAccountId: customer.CustomerAWSAccountId,
customerIdentifier: customer.CustomerIdentifier,
productCode: customer.ProductCode,
},
where: {
customerIdentifier: customer.CustomerIdentifier,
},
});

return "Signup successful!<br>Please check your inbox to complete registration and set your password.";
} catch (error) {
this.logger.error(
`Failed to register an AWS Marketplace purchase`,
error
);
return "Failed to register. Please contact Amplication support";
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export const AWS_MARKETPLACE_INTEGRATION_CALLBACK_PATH =
"/auth/aws-marketplace/callback";
Loading

0 comments on commit 9d2f677

Please sign in to comment.