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:
parent
3f78a1af43
commit
12df937d17
|
|
@ -1,7 +1,5 @@
|
||||||
# Environment variables declared in this file are automatically made available to Prisma.
|
DATABASE_URL="mongodb+srv://root:randompassword@cluster0.ab1cd.mongodb.net/mydb?retryWrites=true&w=majority"
|
||||||
# See the documentation for more detail: https://pris.ly/d/prisma-schema#accessing-environment-variables-from-the-schema
|
BUNNY_CDN_ACCESS_KEY=""
|
||||||
|
CONTRACT_ADDRESS=""
|
||||||
# Prisma supports the native connection string format for PostgreSQL, MySQL, SQLite, SQL Server, MongoDB and CockroachDB.
|
PRIVATE_KEY=""
|
||||||
# See the documentation for all the connection string options: https://pris.ly/d/connection-strings
|
JSON_RPC=""
|
||||||
|
|
||||||
DATABASE_URL="mongodb+srv://root:randompassword@cluster0.ab1cd.mongodb.net/mydb?retryWrites=true&w=majority"
|
|
||||||
|
|
@ -26,3 +26,11 @@ model tokens {
|
||||||
tokenId Int
|
tokenId Int
|
||||||
verified Boolean
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ provider:
|
||||||
runtime: nodejs18.x
|
runtime: nodejs18.x
|
||||||
stage: ${opt:stage, 'prd'}
|
stage: ${opt:stage, 'prd'}
|
||||||
region: ${opt:region, 'us-west-2'}
|
region: ${opt:region, 'us-west-2'}
|
||||||
|
timeout: 40
|
||||||
apiGateway:
|
apiGateway:
|
||||||
minimumCompressionSize: 1024
|
minimumCompressionSize: 1024
|
||||||
shouldStartNameWithService: true
|
shouldStartNameWithService: true
|
||||||
|
|
@ -43,6 +44,13 @@ custom:
|
||||||
concurrency: 10
|
concurrency: 10
|
||||||
|
|
||||||
functions:
|
functions:
|
||||||
|
submitAppInfo:
|
||||||
|
handler: src/functions/apps/handler.submitAppInfo
|
||||||
|
events:
|
||||||
|
- http:
|
||||||
|
path: app
|
||||||
|
method: post
|
||||||
|
cors: true
|
||||||
|
|
||||||
submitBuildInfo:
|
submitBuildInfo:
|
||||||
# Deployment:
|
# Deployment:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import { handlerPath } from '@libs/handler-resolver';
|
||||||
|
|
||||||
|
export const submitAppInfo = {
|
||||||
|
handler: `${handlerPath(__dirname)}/handler.submitAppInfo`,
|
||||||
|
events: [
|
||||||
|
{
|
||||||
|
http: {
|
||||||
|
method: 'post',
|
||||||
|
path: 'app',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -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'] };
|
||||||
|
}
|
||||||
|
};
|
||||||
Loading…
Reference in New Issue