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: bunny cdn integrations #290

Merged
merged 14 commits into from
Jul 13, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion serverless/src/functions/apps/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,4 +179,4 @@ export const submitAppInfo = async (
message: e,
});
}
};
};
11 changes: 10 additions & 1 deletion ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,16 @@ Get them from the project settings on the firebase dashboard. Read [this article
VITE_GOERLI_RPC
```

5. Start the local server running the app:
5. Set the Bunny CDN endpoint and the Sign in Key needed to create the signature

```bash
VITE_BUNNYCDN_URL
VITE_FE_SIGNING_KEY
VITE_BUNNYCDN_CREATE_PULLZONE
VITE_BUNNYCDN_VERIFY_PULLZONE
```

6. Start the local server running the app:

```bash
$ yarn dev
Expand Down
1 change: 1 addition & 0 deletions ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
"@stitches/react": "^1.2.8",
"abitype": "^0.5.0",
"alchemy-sdk": "^2.5.0",
"axios": "^1.4.0",
"colorthief": "^2.3.2",
"connectkit": "^1.1.3",
"firebase": "^9.17.1",
Expand Down
11 changes: 11 additions & 0 deletions ui/src/constants/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,4 +26,15 @@ export const env = Object.freeze({
goerli: {
rpc: import.meta.env.VITE_GOERLI_RPC || '',
},
bunnyCDN: {
url: import.meta.env.VITE_BUNNYCDN_URL || '',
feSigningKey: import.meta.env.VITE_FE_SIGNING_KEY || '',
createPullzone: import.meta.env.VITE_BUNNYCDN_CREATE_PULLZONE || '',
verifyPullzone: import.meta.env.VITE_BUNNYCDN_VERIFY_PULLZONE || '',
errorMessages: {
nameTaken:
import.meta.env.VITE_BUNNYCDN_ERROR_MESSAGE_NAME_TAKEN ||
'pullzone.hostname_already_registered',
},
},
});
4 changes: 2 additions & 2 deletions ui/src/providers/connectkit-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ import {
getDefaultClient,
} from 'connectkit';
import { createClient, WagmiConfig } from 'wagmi';
import { goerli, polygonMumbai } from 'wagmi/chains';
import { goerli } from 'wagmi/chains';

import { env } from '@/constants';

const alchemyId = env.alchemy.id;
const chains = [polygonMumbai];
const chains = [goerli];

const wagmiClient = createClient(
getDefaultClient({
Expand Down
35 changes: 24 additions & 11 deletions ui/src/store/features/bunny-cdn/async-thunk/create-cdn.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,47 @@
import { createAsyncThunk } from '@reduxjs/toolkit';

import { createBunnyCDNMock } from '@/mocks';
import { RootState } from '@/store';
import { AppLog } from '@/utils';

import { bunnyCDNActions } from '../bunny-cdn-slice';
import { BunnyCDNClient } from '../bunny-cdn-client';
import { env } from '@/constants';
import axios, { AxiosError } from 'axios';

type CNAMERecord = {
domain: string;
sourceDomain: string;
targetDomain: string;
};

export const createBunnyCDN = createAsyncThunk<void, CNAMERecord>(
export const createPullzone = createAsyncThunk<void, CNAMERecord>(
'BunnyCDN/CreateCDN',
async ({ domain, targetDomain }, { dispatch, getState }) => {
async ({ sourceDomain, targetDomain }, { dispatch, getState }) => {
const { state } = (getState() as RootState).bunnyCDN;

if (state === 'loading') return;

try {
dispatch(bunnyCDNActions.setState('loading'));

const CDNRecord = await createBunnyCDNMock(domain, targetDomain);

dispatch(bunnyCDNActions.setCDNRecordData(CDNRecord.bunnyURL));
} catch (error) {
AppLog.errorToast(
'Failed to create the CDN record. Please, try again',
error
const CDNRecord = await BunnyCDNClient.createPullzone(
sourceDomain,
targetDomain
);

dispatch(bunnyCDNActions.setCDNRecordData(CDNRecord));
} catch (error: Error | AxiosError | any) {
let message = 'Failed to create the CDN record. Please, try again';

if (
axios.isAxiosError(error) &&
error.response?.data.message.name ===
env.bunnyCDN.errorMessages.nameTaken
) {
message =
'Pullzone name is already taken. Please, try again with a different name';
}

AppLog.errorToast(message, error);
dispatch(bunnyCDNActions.setState('failed'));
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
import { createAsyncThunk } from '@reduxjs/toolkit';

import { verifyBunnyCDNMock } from '@/mocks';
import { RootState } from '@/store';
import { AppLog } from '@/utils';

import { bunnyCDNActions } from '../bunny-cdn-slice';
import { BunnyCDNClient } from '../bunny-cdn-client';

export const verifyBunnyPullzone = createAsyncThunk<void, string>(
'BunnyCDN/VerifyPullzone',
async (domain, { dispatch, getState }): Promise<void> => {
async (hostName, { dispatch, getState }): Promise<void> => {
const { state } = (getState() as RootState).bunnyCDN;

if (state === 'loading') return;

try {
dispatch(bunnyCDNActions.setState('loading'));

const verifyAPState = await verifyBunnyCDNMock(domain);
const verifyAPState = await BunnyCDNClient.verifyPullzone(hostName);

if (verifyAPState) dispatch(bunnyCDNActions.setState('success'));
else throw new Error('Invalid AP state');
} catch (error) {
AppLog.errorToast(
'There was an error trying to verify the domain. Please, try again',
'There was an error trying to verify the hostName. Please, try again',
error
);
dispatch(bunnyCDNActions.setState('failed'));
Expand Down
75 changes: 75 additions & 0 deletions ui/src/store/features/bunny-cdn/bunny-cdn-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { env } from '@/constants';
import axios from 'axios';
import * as crypto from 'crypto';

const client = (body: string, endpoint: string) => {
if (!env.bunnyCDN.feSigningKey) {
throw new Error('Missing BunnyCDN signing key');
}

if (!env.bunnyCDN.url) {
throw new Error('Missing BunnyCDN url');
}

const signature = generateSignature(body, env.bunnyCDN.feSigningKey);

const instance = axios.create({
baseURL: env.bunnyCDN.url,
headers: {
'lambda-signature': signature,
},
});

return instance.post(endpoint, body);
};

const createPullzone = async (sourceDomain: string, targetDomain: string) => {
try {
const body = JSON.stringify({
sourceDomain,
targetDomain,
});

if (!env.bunnyCDN.createPullzone) {
throw new Error('Missing BunnyCDN create pullzone endpoint');
}

const response = await client(body, env.bunnyCDN.createPullzone);

return response.data.appInfo.appId;
} catch (error) {
throw error;
}
};

const verifyPullzone = async (hostName: string) => {
try {
const body = JSON.stringify({
hostName,
});

if (!env.bunnyCDN.verifyPullzone) {
throw new Error('Missing BunnyCDN verify pullzone endpoint');
}

const response = await client(body, env.bunnyCDN.verifyPullzone);

return response.data;
} catch (error) {
throw error;
}
};

export const BunnyCDNClient = {
createPullzone,
verifyPullzone,
};

const generateSignature = (
body: string, // must be raw string body, not json transformed version of the body
signingKey: string // signing secret key for front-end
): string => {
const hmac = crypto.createHmac('sha256', signingKey); // Create a HMAC SHA256 hash using the signing key
hmac.update(body, 'utf8'); // Update the token hash with the request body using utf8
return hmac.digest('hex');
};
Empty file.
16 changes: 8 additions & 8 deletions ui/src/views/access-point/ap-form-step/create-ap-form-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,15 +52,15 @@ export const CreateAccessPointFormBody: React.FC = () => {

const {
form: {
domain: {
value: [domain],
targetDomain: {
value: [targetDomain],
},
isValid: [isValid],
},
} = useAccessPointFormContext();

const {
form: { domain: domainContext },
form: { targetDomain: domainContext },
} = useAccessPointFormContext();

const { loading: nfaLoading } = useQuery(getNFADocument, {
Expand Down Expand Up @@ -108,13 +108,13 @@ export const CreateAccessPointFormBody: React.FC = () => {
return;
}

if (nfa && domain) {
if (nfa && targetDomain) {
try {
setArgs([Number(nfa.tokenId), domain, { value: billing }]);
setArgs([Number(nfa.tokenId), targetDomain, { value: billing }]);
dispatch(
bunnyCDNActions.createBunnyCDN({
domain: 'domain',
targetDomain: domain,
bunnyCDNActions.createPullzone({
sourceDomain: nfa.externalURL,
targetDomain: targetDomain,
})
);
} catch (e) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { createContext, StringValidators } from '@/utils';

export type CreateAccessPointFormContext = {
form: {
domain: FormField;
targetDomain: FormField;
isValid: ReactState<boolean>;
};
};
Expand All @@ -20,7 +20,7 @@ export const [CreateAccessPointFormProvider, useAccessPointFormContext] =
export const useAccessPointFormContextInit =
(): CreateAccessPointFormContext => ({
form: {
domain: useFormField('domain', [
targetDomain: useFormField('targetDomain', [
StringValidators.required,
StringValidators.isValidDomain,
]),
Expand Down
17 changes: 7 additions & 10 deletions ui/src/views/access-point/ap-record-step/ap-record-body.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,28 +4,25 @@ import { Button, Card, Flex, SpinnerDot, Stepper, Text } from '@/components';
import { bunnyCDNActions, useAppDispatch, useBunnyCDNStore } from '@/store';

import { useAccessPointFormContext } from '../ap-form-step';
import { CreateAccessPoint } from '../create-ap.context';
import { DisplayText } from '../display-text';
import { isSubdomain } from './record-step.utils';

export const APRecordCardBody: React.FC = () => {
const dispatch = useAppDispatch();
const { bunnyURL, state } = useBunnyCDNStore();
const {
nfa: { domain: nfaDomain },
} = CreateAccessPoint.useContext();

const {
form: {
domain: {
value: [accesPointDomain],
targetDomain: {
value: [targetDomain],
},
},
} = useAccessPointFormContext();
const { nextStep } = Stepper.useContext();

const isSudomain = useMemo(
() => isSubdomain(accesPointDomain),
[accesPointDomain]
() => isSubdomain(targetDomain),
[targetDomain]
);

useEffect(() => {
Expand All @@ -36,7 +33,7 @@ export const APRecordCardBody: React.FC = () => {
}, [state, nextStep, dispatch]);

const handleContinueClick = (): void => {
dispatch(bunnyCDNActions.verifyBunnyPullzone(nfaDomain));
dispatch(bunnyCDNActions.verifyBunnyPullzone(targetDomain));
};

return (
Expand Down Expand Up @@ -66,7 +63,7 @@ export const APRecordCardBody: React.FC = () => {
value={isSudomain ? 'CNAME' : 'ANAME'}
/>
<DisplayText label="Host" value={isSudomain ? 'App' : '@'} />
<DisplayText label="Data (Points to)" value={bunnyURL} />
<DisplayText label="Data (Points to)" value={`${bunnyURL}.b-cdn.net`} />
<Button
colorScheme="blue"
variant="solid"
Expand Down
2 changes: 1 addition & 1 deletion ui/src/views/access-point/create-ap-preview.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const AccessPointDataFragment: React.FC = () => {
const { address, status } = useAccount();
const {
form: {
domain: {
targetDomain: {
value: [domain],
},
},
Expand Down
6 changes: 3 additions & 3 deletions ui/src/views/access-point/create-ap.stepper.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export const CreateApStepper: React.FC = () => {
} = CreateAccessPoint.useTransactionContext();
const {
form: {
domain: {
value: [accesPointDomain],
targetDomain: {
value: [targetDomain],
},
isValid: [, setIsValid],
},
Expand All @@ -43,7 +43,7 @@ export const CreateApStepper: React.FC = () => {
<Stepper.Step>
<Step
header={`Add a ${
isSubdomain(accesPointDomain) ? 'CNAME' : 'ANAME'
isSubdomain(targetDomain) ? 'CNAME' : 'ANAME'
} record to your DNS provider`}
>
<APRecordStep />
Expand Down
Loading
Loading