feat: bunny cdn integration and create pull zone (#264)

* feat: bunny cdn integration and create pull zone setup

* feat: uncomment implementation.

* feat: handle repeated ids, update schema to contain more fields, fix the bugs.

* refactor: rename access points - > apps.

* feat: handle the possibility of infinite loop and remove unnecessariy functions.
This commit is contained in:
Shredder 2023-06-09 22:13:47 +03:30 committed by GitHub
parent 3f78a1af43
commit 12df937d17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 385 additions and 7 deletions

View File

@ -1,7 +1,5 @@
# Environment variables declared in this file are automatically made available to Prisma.
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
DATABASE_URL="mongodb+srv://root:randompassword@cluster0.ab1cd.mongodb.net/mydb?retryWrites=true&w=majority"
DATABASE_URL="mongodb+srv://root:randompassword@cluster0.ab1cd.mongodb.net/mydb?retryWrites=true&w=majority"
BUNNY_CDN_ACCESS_KEY=""
CONTRACT_ADDRESS=""
PRIVATE_KEY=""
JSON_RPC=""

View File

@ -26,3 +26,11 @@ model tokens {
tokenId Int
verified Boolean
}
model zones {
id String @id @default(auto()) @map("_id") @db.ObjectId
zoneId Int // The returned id from the creation call
name String // The assigned name at the time of creation
hostname String // The target domain that's assigned as hostname
sourceDomain String // The origin URL
}

View File

@ -11,6 +11,7 @@ provider:
runtime: nodejs18.x
stage: ${opt:stage, 'prd'}
region: ${opt:region, 'us-west-2'}
timeout: 40
apiGateway:
minimumCompressionSize: 1024
shouldStartNameWithService: true
@ -43,6 +44,13 @@ custom:
concurrency: 10
functions:
submitAppInfo:
handler: src/functions/apps/handler.submitAppInfo
events:
- http:
path: app
method: post
cors: true
submitBuildInfo:
# Deployment:

View File

@ -0,0 +1,107 @@
import { APIGatewayProxyResult, APIGatewayEvent } from 'aws-lambda';
import { formatJSONResponse } from '@libs/api-gateway';
import * as dotenv from 'dotenv';
import { v4 } from 'uuid';
import { prisma } from '@libs/prisma';
import {
BunnyCdn,
BunnyCdnError,
CreatePullZoneMethodArgs,
} from '@libs/bunnyCDN';
export const submitAppInfo = async (
event: APIGatewayEvent
): Promise<APIGatewayProxyResult> => {
try {
// Check the parameters and environment variables
dotenv.config();
if (event.body === null || process.env.BUNNY_CDN_ACCESS_KEY == undefined) {
return formatJSONResponse({
status: 422,
message: 'Required parameters were not passed.',
});
}
// Set up constants
const bunnyCdn = new BunnyCdn(process.env.BUNNY_CDN_ACCESS_KEY);
const data = JSON.parse(event.body);
const appInfo = {
apId: 'null',
createdAt: new Date().toISOString(),
sourceDomain: data.sourceDomain,
hostname: data.targetDomain,
};
let maxTries = 5;
let pullZone: {
id: any;
name?: string;
originUrl?: string;
hostname?: string;
};
do {
let id = v4();
let requestArgs: CreatePullZoneMethodArgs = {
zoneId: id, // this is technically the zone name. It should be unique.
originUrl: appInfo.sourceDomain,
};
try {
pullZone = await bunnyCdn.createPullZone(requestArgs);
appInfo.apId = id;
} catch (error) {
maxTries -= 1;
if (
error instanceof BunnyCdnError &&
error.name === 'pullzone.name_taken'
) {
continue;
} else if (maxTries == 0) {
throw 'Max number of tries for creating pullzone was reached.';
} else {
throw error;
}
}
} while (maxTries > 0);
// Create custom hostname
await bunnyCdn
.addCustomHostname({
pullZoneId: pullZone!.id,
hostname: appInfo.hostname,
})
.catch((e) => {
throw e;
});
// Add record to the database, if it's not been already added
const zoneRecord = await prisma.zones.findMany({
where: {
zoneId: pullZone!.id,
name: appInfo.apId,
sourceDomain: appInfo.sourceDomain,
},
});
if (zoneRecord.length == 0) {
await prisma.zones.create({
data: {
zoneId: pullZone!.id,
name: appInfo.apId,
hostname: appInfo.hostname,
sourceDomain: appInfo.sourceDomain,
},
});
}
return formatJSONResponse({
appInfo,
});
} catch (e) {
return formatJSONResponse({
status: 500,
message: e,
});
}
};

View File

@ -0,0 +1,13 @@
import { handlerPath } from '@libs/handler-resolver';
export const submitAppInfo = {
handler: `${handlerPath(__dirname)}/handler.submitAppInfo`,
events: [
{
http: {
method: 'post',
path: 'app',
},
},
],
};

View File

@ -0,0 +1,244 @@
import axios, { AxiosRequestConfig, AxiosError } from 'axios';
type BunnyCdnErrorOptions = {
name: string;
message: string;
};
export class BunnyCdnError extends Error {
public name: string;
constructor({ name, message }: BunnyCdnErrorOptions) {
super(message);
this.name = name;
this.message = message;
}
}
const BUNNY_CDN_API_URL = 'https://api.bunny.net';
export class BunnyCdn {
constructor(private accessKey: string) {}
private enforceHttps = (url: string) => {
if (url.startsWith('https://')) {
return url;
}
if (url.startsWith('http://')) {
return url.replace('http://', 'https://');
}
return `https://${url}`;
};
private fetchBunny = async (endpoint: string, init: AxiosRequestConfig) => {
const headers = {
AccessKey: this.accessKey,
};
const response = await axios.request({
url: `${BUNNY_CDN_API_URL}${endpoint}`,
method: 'POST',
headers,
validateStatus: (status) => status < 500,
timeout: 20_000,
...init,
});
const data = response.data;
if (data.ErrorKey || data.Message) {
throw new BunnyCdnError({ name: data.ErrorKey, message: data.Message });
}
return data;
};
public async getPullZone(options: GetPullZoneMethodArgs) {
const data = (await this.fetchBunny(`/pullzone/${options.pullZoneId}`, {
method: 'GET',
})) as PullZoneData;
return {
id: data.Id,
name: data.Name,
originUrl: data.OriginUrl,
hostnames: data.Hostnames,
};
}
public async createPullZone(options: CreatePullZoneMethodArgs) {
const httpsOriginUrl = this.enforceHttps(options.originUrl);
const data = (await this.fetchBunny(`/pullzone`, {
data: {
Name: options.zoneId,
Type: 0,
OriginUrl: httpsOriginUrl,
UseStaleWhileOffline: true,
},
})) as PullZoneData;
const systemHostname: HostnameInterface[] = data.Hostnames.filter(
(hostname) => hostname.IsSystemHostname === true
);
return {
id: data.Id,
name: data.Name,
originUrl: data.OriginUrl,
hostname: systemHostname[0].Value,
};
}
public async updatePullZone(options: UpdatePullZoneMethodArgs) {
const httpsOriginUrl = this.enforceHttps(options.originUrl);
await this.fetchBunny(`/pullzone/${options.pullZoneId}`, {
data: {
OriginUrl: httpsOriginUrl,
},
});
return true;
}
public async deletePullZone(options: DeletePullZoneMethodArgs) {
await this.fetchBunny(`/pullzone/${options.pullZoneId}`, {
method: 'DELETE',
});
return true;
}
public async addCustomHostname(options: AddCustomHostnameMethodArgs) {
await this.fetchBunny(`/pullzone/${options.pullZoneId}/addHostname`, {
data: {
Hostname: options.hostname,
},
});
return true;
}
public async removeCustomHostname(options: AddCustomHostnameMethodArgs) {
await this.fetchBunny(`/pullzone/${options.pullZoneId}/removeHostname`, {
method: 'DELETE',
data: {
Hostname: options.hostname,
},
});
return true;
}
public async loadFreeCertificate(options: LoadFreeCertificateMethodArgs) {
await this.fetchBunny(
`/pullzone/loadFreeCertificate?hostname=${options.hostname}`,
{
method: 'GET',
}
);
return true;
}
public async setForceSSL(options: SetForceSSLMethodArgs) {
await this.fetchBunny(`/pullzone/${options.pullZoneId}/setForceSSL`, {
data: {
Hostname: options.hostname,
ForceSSL: options.shouldForceSSL ?? true,
},
});
return true;
}
public async purgePullZoneCache(options: PurgePullZoneCacheMethodArgs) {
await this.fetchBunny(`/pullzone/${options.pullZoneId}/purgeCache`, {});
return true;
}
}
export type ErrorData = {
ErrorKey: string;
Field: string;
Message: string;
};
export type GetPullZoneMethodArgs = {
pullZoneId: string;
};
export type CreatePullZoneMethodArgs = {
zoneId: string;
originUrl: string;
};
export type UpdatePullZoneMethodArgs = {
pullZoneId: string;
originUrl: string;
};
export type DeletePullZoneMethodArgs = {
pullZoneId: string;
};
export type AddCustomHostnameMethodArgs = {
pullZoneId: string;
hostname: string;
};
export type LoadFreeCertificateMethodArgs = {
hostname: string;
};
export type RemoveCustomHostnameMethodArgs = {
pullZoneId: string;
hostname: string;
};
export type SetForceSSLMethodArgs = {
pullZoneId: string;
hostname: string;
shouldForceSSL?: boolean;
};
export type PurgePullZoneCacheMethodArgs = {
pullZoneId: string;
};
type HostnameInterface = {
/** The unique ID of the hostname */
Id: number;
/** The hostname value for the domain name */
Value: string;
/** Determines if the Force SSL feature is enabled */
ForceSSL: string;
/** Determines if this is a system hostname controlled by bunny.net */
IsSystemHostname: boolean;
/** Determines if the hostname has an SSL certificate configured */
HasCertificate: true;
};
type PullZoneData = {
Id: number;
Name: string;
OriginUrl: string;
Hostnames: HostnameInterface[];
};
export type FetchPullZoneArgs = {
name: string;
};
export const fetchPullZone = async ({ name }: FetchPullZoneArgs) => {
const hostname = `https://${name}.b-cdn.net`;
const response = await axios.head(hostname, { timeout: 20_000 });
if (response.headers['cdn-pullzone']) {
return { hostname, id: response.headers['cdn-pullzone'] };
}
};