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.
|
||||
# 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=""
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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